ass_editor/core/
document.rs

1//! Main document type for the editor
2//!
3//! Provides the `EditorDocument` struct which manages ASS script content
4//! with direct access to parsed ASS structures and efficient text editing.
5
6use super::errors::{EditorError, Result};
7use super::history::UndoManager;
8use super::position::{LineColumn, Position, Range};
9use crate::commands::CommandResult;
10#[cfg(feature = "stream")]
11use ass_core::parser::script::ScriptDeltaOwned;
12use ass_core::parser::{ast::Section, Script};
13use core::ops::Range as StdRange;
14
15#[cfg(feature = "std")]
16use std::sync::Arc;
17
18#[cfg(not(feature = "std"))]
19use alloc::{
20    format,
21    string::{String, ToString},
22    vec,
23    vec::Vec,
24};
25
26#[cfg(feature = "std")]
27use std::sync::mpsc::Sender;
28
29#[cfg(feature = "std")]
30use crate::events::DocumentEvent;
31
32#[cfg(feature = "std")]
33type EventSender = Sender<DocumentEvent>;
34
35/// Main document container for editing ASS scripts
36///
37/// Manages both text content and parsed ASS structures with direct access to
38/// events, styles, and script info without manual parsing.
39#[derive(Debug)]
40pub struct EditorDocument {
41    /// Rope for efficient text editing operations
42    #[cfg(feature = "rope")]
43    text_rope: ropey::Rope,
44
45    /// Raw text content (fallback when rope feature is disabled)
46    #[cfg(not(feature = "rope"))]
47    text_content: String,
48
49    /// Document identifier for session management
50    id: String,
51
52    /// Whether document has unsaved changes
53    modified: bool,
54
55    /// Optional file path if loaded from/saved to disk
56    file_path: Option<String>,
57
58    /// Extension registry integration for tag and section handling
59    #[cfg(feature = "plugins")]
60    registry_integration: Option<Arc<crate::extensions::registry_integration::RegistryIntegration>>,
61
62    /// Undo/redo manager
63    history: UndoManager,
64
65    /// Event channel for sending document events
66    #[cfg(feature = "std")]
67    event_tx: Option<EventSender>,
68
69    /// Incremental parser for efficient updates
70    #[cfg(feature = "stream")]
71    incremental_parser: crate::core::incremental::IncrementalParser,
72
73    /// Lazy validator for on-demand validation
74    validator: crate::utils::validator::LazyValidator,
75}
76
77impl EditorDocument {
78    /// Create a new empty document
79    pub fn new() -> Self {
80        Self {
81            #[cfg(feature = "rope")]
82            text_rope: ropey::Rope::new(),
83            #[cfg(not(feature = "rope"))]
84            text_content: String::new(),
85            id: Self::generate_id(),
86            modified: false,
87            file_path: None,
88            #[cfg(feature = "plugins")]
89            registry_integration: None,
90            history: UndoManager::new(),
91            #[cfg(feature = "std")]
92            event_tx: None,
93            #[cfg(feature = "stream")]
94            incremental_parser: crate::core::incremental::IncrementalParser::new(),
95            validator: crate::utils::validator::LazyValidator::new(),
96        }
97    }
98
99    /// Create a new document with event channel
100    #[cfg(feature = "std")]
101    pub fn with_event_channel(event_tx: EventSender) -> Self {
102        let mut doc = Self::new();
103        doc.event_tx = Some(event_tx);
104        doc
105    }
106
107    /// Create document from file path
108    #[cfg(feature = "std")]
109    pub fn from_file(path: &str) -> Result<Self> {
110        use std::fs;
111        let content = fs::read_to_string(path).map_err(|e| EditorError::IoError(e.to_string()))?;
112        let mut doc = Self::from_content(&content)?;
113        doc.file_path = Some(path.to_string());
114        Ok(doc)
115    }
116
117    /// Save document to file
118    #[cfg(feature = "std")]
119    pub fn save(&mut self) -> Result<()> {
120        if let Some(path) = self.file_path.clone() {
121            self.save_to_file(&path)
122        } else {
123            Err(EditorError::IoError(
124                "No file path set for document".to_string(),
125            ))
126        }
127    }
128
129    /// Save document to specific file path
130    #[cfg(feature = "std")]
131    pub fn save_to_file(&mut self, path: &str) -> Result<()> {
132        use std::fs;
133        let content = self.text();
134        fs::write(path, content).map_err(|e| EditorError::IoError(e.to_string()))?;
135        self.modified = false;
136        self.file_path = Some(path.to_string());
137        Ok(())
138    }
139
140    /// Create document with specific ID
141    pub fn with_id(id: String) -> Self {
142        let mut doc = Self::new();
143        doc.id = id;
144        doc
145    }
146
147    /// Emit an event to the event channel
148    #[cfg(feature = "std")]
149    fn emit(&mut self, event: DocumentEvent) {
150        if let Some(tx) = &mut self.event_tx {
151            let _ = tx.send(event);
152        }
153    }
154
155    /// Set the event channel for this document
156    #[cfg(feature = "std")]
157    pub fn set_event_channel(&mut self, event_tx: EventSender) {
158        self.event_tx = Some(event_tx);
159    }
160
161    /// Check if document has an event channel
162    #[cfg(feature = "std")]
163    pub fn has_event_channel(&self) -> bool {
164        self.event_tx.is_some()
165    }
166
167    /// Load document from string content
168    ///
169    /// Creates a new `EditorDocument` from ASS subtitle content. The content
170    /// is validated during creation to ensure it's parseable.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use ass_editor::EditorDocument;
176    ///
177    /// let content = r#"
178    /// [Script Info]
179    /// Title: My Subtitle
180    ///
181    /// [V4+ Styles]
182    /// Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
183    /// Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
184    ///
185    /// [Events]
186    /// Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
187    /// Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello World
188    /// "#;
189    ///
190    /// let doc = EditorDocument::from_content(content).unwrap();
191    /// assert!(doc.text().contains("Hello World"));
192    /// ```
193    ///
194    /// # Errors
195    ///
196    /// Returns `Err` if the content cannot be parsed as valid ASS format.
197    pub fn from_content(content: &str) -> Result<Self> {
198        // Validate that content can be parsed
199        let _ = Script::parse(content).map_err(EditorError::from)?;
200
201        #[cfg(feature = "stream")]
202        let mut incremental_parser = crate::core::incremental::IncrementalParser::new();
203        #[cfg(feature = "stream")]
204        incremental_parser.initialize_cache(content);
205
206        Ok(Self {
207            #[cfg(feature = "rope")]
208            text_rope: ropey::Rope::from_str(content),
209            #[cfg(not(feature = "rope"))]
210            text_content: content.to_string(),
211            id: Self::generate_id(),
212            modified: false,
213            file_path: None,
214            #[cfg(feature = "plugins")]
215            registry_integration: None,
216            history: UndoManager::new(),
217            #[cfg(feature = "std")]
218            event_tx: None,
219            #[cfg(feature = "stream")]
220            incremental_parser,
221            validator: crate::utils::validator::LazyValidator::new(),
222        })
223    }
224
225    /// Parse the current document content into a Script
226    ///
227    /// Returns a boxed closure that provides the parsed Script.
228    /// This avoids lifetime issues by ensuring the content outlives the Script.
229    pub fn parse_script_with<F, R>(&self, f: F) -> Result<R>
230    where
231        F: FnOnce(&Script) -> R,
232    {
233        let content = self.text();
234        match Script::parse(&content) {
235            Ok(script) => Ok(f(&script)),
236            Err(e) => Err(EditorError::from(e)),
237        }
238    }
239
240    /// Validate the document content can be parsed as valid ASS
241    /// This is the basic validation that just checks parsing
242    pub fn validate(&self) -> Result<()> {
243        let content = self.text();
244        Script::parse(&content).map_err(EditorError::from)?;
245        Ok(())
246    }
247
248    /// Execute a command with proper history recording
249    ///
250    /// This method ensures that commands are properly recorded in the undo history.
251    /// Use this instead of calling command.execute() directly if you want undo support.
252    ///
253    /// For BatchCommand, this creates a synthetic undo operation that captures
254    /// the aggregate effect of all sub-commands.
255    pub fn execute_command(
256        &mut self,
257        command: &dyn crate::commands::EditorCommand,
258    ) -> Result<crate::commands::CommandResult> {
259        use crate::core::history::Operation;
260
261        // Get the cursor position before
262        let _cursor_before = self.cursor_position();
263
264        // Execute the command
265        let result = command.execute(self)?;
266
267        // Only record if the command changed content
268        if result.content_changed {
269            // Determine the operation based on the result
270            let operation = if let Some(range) = result.modified_range {
271                if range.is_empty() {
272                    // This was an insertion
273                    let inserted_text = self.text_range(Range::new(
274                        range.start,
275                        Position::new(
276                            range.start.offset
277                                + result
278                                    .new_cursor
279                                    .map_or(0, |c| c.offset - range.start.offset),
280                        ),
281                    ))?;
282                    Operation::Insert {
283                        position: range.start,
284                        text: inserted_text,
285                    }
286                } else {
287                    // For batch commands and complex operations, we need to be more careful
288                    // Store enough information to properly undo
289                    // This is a simplified approach - ideally each command would handle its own undo
290                    let cmd_desc = command.description();
291                    if cmd_desc.contains("batch") || cmd_desc.contains("Multiple") {
292                        // For batch operations, we don't have enough info
293                        // Just use a simple insert operation
294                        Operation::Insert {
295                            position: range.start,
296                            text: String::new(),
297                        }
298                    } else {
299                        // For simple operations, use the range info
300                        Operation::Replace {
301                            range,
302                            old_text: String::new(), // We don't have the old text
303                            new_text: self.text_range(range).unwrap_or_default(),
304                        }
305                    }
306                }
307            } else {
308                // No range info - create a generic operation
309                Operation::Insert {
310                    position: Position::new(0),
311                    text: String::new(),
312                }
313            };
314
315            // Update cursor if needed
316            if let Some(new_cursor) = result.new_cursor {
317                self.set_cursor_position(Some(new_cursor));
318            }
319
320            // Record in history
321            self.history
322                .record_operation(operation, command.description().to_string(), &result);
323
324            // Clear validation cache since content changed
325            self.validator.clear_cache();
326        }
327
328        Ok(result)
329    }
330
331    /// Perform comprehensive validation using the LazyValidator
332    /// Returns detailed validation results including warnings and suggestions
333    ///
334    /// Note: Returns a cloned result to avoid borrow checker issues
335    pub fn validate_comprehensive(&mut self) -> Result<crate::utils::validator::ValidationResult> {
336        // Create a temporary document reference for validation
337        // This is needed because we can't pass &self while mutably borrowing validator
338        let temp_doc = EditorDocument::from_content(&self.text())?;
339        let result = self.validator.validate(&temp_doc)?;
340        Ok(result.clone())
341    }
342
343    /// Force revalidation even if cached results exist
344    pub fn force_validate(&mut self) -> Result<crate::utils::validator::ValidationResult> {
345        // Create a temporary document reference for validation
346        let temp_doc = EditorDocument::from_content(&self.text())?;
347        let result = self.validator.force_validate(&temp_doc)?;
348        Ok(result.clone())
349    }
350
351    /// Check if document is valid (quick check using cache if available)
352    pub fn is_valid_cached(&mut self) -> Result<bool> {
353        // Create a temporary document reference for validation
354        let temp_doc = EditorDocument::from_content(&self.text())?;
355        self.validator.is_valid(&temp_doc)
356    }
357
358    /// Get cached validation result without revalidating
359    pub fn validation_result(&self) -> Option<&crate::utils::validator::ValidationResult> {
360        self.validator.cached_result()
361    }
362
363    /// Configure the validator
364    pub fn set_validator_config(&mut self, config: crate::utils::validator::ValidatorConfig) {
365        self.validator.set_config(config);
366    }
367
368    /// Get mutable access to the validator
369    pub fn validator_mut(&mut self) -> &mut crate::utils::validator::LazyValidator {
370        &mut self.validator
371    }
372
373    /// Get document ID
374    #[must_use]
375    pub fn id(&self) -> &str {
376        &self.id
377    }
378
379    /// Get file path if document is associated with a file
380    #[must_use]
381    pub fn file_path(&self) -> Option<&str> {
382        self.file_path.as_deref()
383    }
384
385    /// Set file path for the document
386    pub fn set_file_path(&mut self, path: Option<String>) {
387        self.file_path = path;
388    }
389
390    /// Import content from another subtitle format
391    #[cfg(feature = "formats")]
392    pub fn import_format(
393        content: &str,
394        format: Option<crate::utils::formats::SubtitleFormat>,
395    ) -> Result<Self> {
396        let ass_content = crate::utils::formats::FormatConverter::import(content, format)?;
397        Self::from_content(&ass_content)
398    }
399
400    /// Export document to another subtitle format
401    #[cfg(feature = "formats")]
402    pub fn export_format(
403        &self,
404        format: crate::utils::formats::SubtitleFormat,
405        options: &crate::utils::formats::ConversionOptions,
406    ) -> Result<String> {
407        crate::utils::formats::FormatConverter::export(self, format, options)
408    }
409
410    /// Check if document has unsaved changes
411    #[must_use]
412    pub const fn is_modified(&self) -> bool {
413        self.modified
414    }
415
416    /// Get current cursor position (if tracked)
417    #[must_use]
418    pub fn cursor_position(&self) -> Option<Position> {
419        self.history.cursor_position()
420    }
421
422    /// Set current cursor position for tracking
423    pub fn set_cursor_position(&mut self, position: Option<Position>) {
424        self.history.set_cursor(position);
425    }
426
427    /// Mark document as modified
428    pub fn set_modified(&mut self, modified: bool) {
429        self.modified = modified;
430    }
431
432    /// Get total length in bytes
433    #[must_use]
434    pub fn len_bytes(&self) -> usize {
435        #[cfg(feature = "rope")]
436        {
437            self.text_rope.len_bytes()
438        }
439        #[cfg(not(feature = "rope"))]
440        {
441            self.text_content.len()
442        }
443    }
444
445    /// Get total number of lines
446    #[must_use]
447    pub fn len_lines(&self) -> usize {
448        #[cfg(feature = "rope")]
449        {
450            self.text_rope.len_lines()
451        }
452        #[cfg(not(feature = "rope"))]
453        {
454            self.text_content.lines().count().max(1)
455        }
456    }
457
458    /// Check if document is empty
459    #[must_use]
460    pub fn is_empty(&self) -> bool {
461        self.len_bytes() == 0
462    }
463
464    /// Get text content as string
465    #[must_use]
466    pub fn text(&self) -> String {
467        #[cfg(feature = "rope")]
468        {
469            self.text_rope.to_string()
470        }
471        #[cfg(not(feature = "rope"))]
472        {
473            self.text_content.clone()
474        }
475    }
476
477    /// Get direct access to the rope for advanced operations
478    #[cfg(feature = "rope")]
479    #[must_use]
480    pub fn rope(&self) -> &ropey::Rope {
481        &self.text_rope
482    }
483
484    /// Get the length of the document in bytes
485    #[must_use]
486    pub fn len(&self) -> usize {
487        #[cfg(feature = "rope")]
488        {
489            self.text_rope.len_bytes()
490        }
491        #[cfg(not(feature = "rope"))]
492        {
493            self.text_content.len()
494        }
495    }
496
497    /// Get text content for a range
498    pub fn text_range(&self, range: Range) -> Result<String> {
499        let start = range.start.offset;
500        let end = range.end.offset;
501
502        if end > self.len_bytes() {
503            return Err(EditorError::InvalidRange {
504                start,
505                end,
506                length: self.len_bytes(),
507            });
508        }
509
510        #[cfg(feature = "rope")]
511        {
512            // Convert byte offsets to char indices for rope operations
513            let start_char = self.text_rope.byte_to_char(start);
514            let end_char = self.text_rope.byte_to_char(end);
515            Ok(self.text_rope.slice(start_char..end_char).to_string())
516        }
517        #[cfg(not(feature = "rope"))]
518        {
519            Ok(self.text_content[start..end].to_string())
520        }
521    }
522
523    /// Convert byte position to line/column
524    #[cfg(feature = "rope")]
525    pub fn position_to_line_column(&self, pos: Position) -> Result<LineColumn> {
526        if pos.offset > self.len_bytes() {
527            return Err(EditorError::PositionOutOfBounds {
528                position: pos.offset,
529                length: self.len_bytes(),
530            });
531        }
532
533        let line_idx = self.text_rope.byte_to_line(pos.offset);
534        let line_start = self.text_rope.line_to_byte(line_idx);
535        let col_offset = pos.offset - line_start;
536
537        // Convert byte offset to character offset within line
538        let line = self.text_rope.line(line_idx);
539        let mut char_col = 0;
540        let mut byte_count = 0;
541
542        for ch in line.chars() {
543            if byte_count >= col_offset {
544                break;
545            }
546            byte_count += ch.len_utf8();
547            char_col += 1;
548        }
549
550        // Convert to 1-indexed
551        LineColumn::new(line_idx + 1, char_col + 1)
552    }
553
554    /// Convert byte position to line/column (without rope)
555    #[cfg(not(feature = "rope"))]
556    pub fn position_to_line_column(&self, pos: Position) -> Result<LineColumn> {
557        if pos.offset > self.len_bytes() {
558            return Err(EditorError::PositionOutOfBounds {
559                position: pos.offset,
560                length: self.len_bytes(),
561            });
562        }
563
564        let mut line = 1;
565        let mut col = 1;
566        let mut byte_pos = 0;
567
568        for ch in self.text_content.chars() {
569            if byte_pos >= pos.offset {
570                break;
571            }
572
573            if ch == '\n' {
574                line += 1;
575                col = 1;
576            } else {
577                col += 1;
578            }
579
580            byte_pos += ch.len_utf8();
581        }
582
583        LineColumn::new(line, col)
584    }
585
586    /// Insert text at position (low-level operation without undo)
587    pub(crate) fn insert_raw(&mut self, pos: Position, text: &str) -> Result<()> {
588        if pos.offset > self.len_bytes() {
589            return Err(EditorError::PositionOutOfBounds {
590                position: pos.offset,
591                length: self.len_bytes(),
592            });
593        }
594
595        #[cfg(feature = "rope")]
596        {
597            // Convert byte offset to char index for rope operations
598            let char_idx = self.text_rope.byte_to_char(pos.offset);
599            self.text_rope.insert(char_idx, text);
600        }
601        #[cfg(not(feature = "rope"))]
602        {
603            self.text_content.insert_str(pos.offset, text);
604        }
605
606        self.modified = true;
607        Ok(())
608    }
609
610    /// Insert text at position with undo support
611    ///
612    /// Inserts text at the given position, automatically updating the underlying
613    /// text representation and recording the operation in the undo history.
614    ///
615    /// # Examples
616    ///
617    /// ```
618    /// use ass_editor::{EditorDocument, Position};
619    ///
620    /// let mut doc = EditorDocument::from_content("Hello World").unwrap();
621    /// let pos = Position::new(5); // Insert after "Hello"
622    /// doc.insert(pos, " there").unwrap();
623    ///
624    /// assert_eq!(doc.text(), "Hello there World");
625    ///
626    /// // Can undo the operation
627    /// doc.undo().unwrap();
628    /// assert_eq!(doc.text(), "Hello World");
629    /// ```
630    ///
631    /// # Errors
632    ///
633    /// Returns `Err` if the position is beyond the document bounds.
634    pub fn insert(&mut self, pos: Position, text: &str) -> Result<()> {
635        use crate::commands::{EditorCommand, InsertTextCommand};
636        use crate::core::history::Operation;
637
638        let command = InsertTextCommand::new(pos, text.to_string());
639        let result = command.execute(self)?;
640
641        // Record the operation in history
642        let operation = Operation::Insert {
643            position: pos,
644            text: text.to_string(),
645        };
646        self.history
647            .record_operation(operation, command.description().to_string(), &result);
648
649        // Clear validation cache since content changed
650        self.validator.clear_cache();
651
652        // Emit event
653        #[cfg(feature = "std")]
654        self.emit(DocumentEvent::TextInserted {
655            position: pos,
656            text: text.to_string(),
657            length: text.len(),
658        });
659
660        Ok(())
661    }
662
663    /// Delete text in range with undo support
664    pub fn delete(&mut self, range: Range) -> Result<()> {
665        use crate::commands::{DeleteTextCommand, EditorCommand};
666        use crate::core::history::Operation;
667
668        // Capture the text that will be deleted BEFORE deletion
669        let deleted_text = self.text_range(range)?;
670
671        let command = DeleteTextCommand::new(range);
672        let result = command.execute(self)?;
673
674        // Record the operation in history
675        let operation = Operation::Delete {
676            range,
677            deleted_text: deleted_text.clone(),
678        };
679        self.history
680            .record_operation(operation, command.description().to_string(), &result);
681
682        // Clear validation cache since content changed
683        self.validator.clear_cache();
684
685        // Emit event
686        #[cfg(feature = "std")]
687        self.emit(DocumentEvent::TextDeleted {
688            range,
689            deleted_text,
690        });
691
692        Ok(())
693    }
694
695    /// Replace text in range with undo support
696    pub fn replace(&mut self, range: Range, text: &str) -> Result<()> {
697        use crate::commands::{EditorCommand, ReplaceTextCommand};
698        use crate::core::history::Operation;
699
700        // Capture the old text BEFORE replacement
701        let old_text = self.text_range(range)?;
702
703        let command = ReplaceTextCommand::new(range, text.to_string());
704        let result = command.execute(self)?;
705
706        // Record the operation in history
707        let operation = Operation::Replace {
708            range,
709            old_text: old_text.clone(),
710            new_text: text.to_string(),
711        };
712        self.history
713            .record_operation(operation, command.description().to_string(), &result);
714
715        // Clear validation cache since content changed
716        self.validator.clear_cache();
717
718        // Emit event
719        #[cfg(feature = "std")]
720        self.emit(DocumentEvent::TextReplaced {
721            range,
722            old_text,
723            new_text: text.to_string(),
724        });
725
726        Ok(())
727    }
728
729    /// Generate unique document ID
730    fn generate_id() -> String {
731        // Simple ID generation - in production might use UUID
732        #[cfg(feature = "std")]
733        {
734            use std::time::{SystemTime, UNIX_EPOCH};
735            let timestamp = SystemTime::now()
736                .duration_since(UNIX_EPOCH)
737                .unwrap_or_default()
738                .as_nanos();
739            format!("doc_{timestamp}")
740        }
741        #[cfg(not(feature = "std"))]
742        {
743            static mut COUNTER: u32 = 0;
744            #[allow(static_mut_refs)]
745            unsafe {
746                COUNTER += 1;
747                format!("doc_{COUNTER}")
748            }
749        }
750    }
751
752    // === ASS-Aware APIs ===
753
754    /// Get number of events without manual parsing  
755    pub fn events_count(&self) -> Result<usize> {
756        self.parse_script_with(|script| {
757            let mut count = 0;
758            for section in script.sections() {
759                if let Section::Events(events) = section {
760                    count += events.len();
761                }
762            }
763            count
764        })
765    }
766
767    /// Get number of styles without manual parsing
768    pub fn styles_count(&self) -> Result<usize> {
769        self.parse_script_with(|script| {
770            let mut count = 0;
771            for section in script.sections() {
772                if let Section::Styles(styles) = section {
773                    count += styles.len();
774                }
775            }
776            count
777        })
778    }
779
780    /// Get script info field names
781    pub fn script_info_fields(&self) -> Result<Vec<String>> {
782        self.parse_script_with(|script| {
783            let mut fields = Vec::new();
784            for section in script.sections() {
785                if let Section::ScriptInfo(info) = section {
786                    for (key, _) in &info.fields {
787                        fields.push(key.to_string());
788                    }
789                }
790            }
791            fields
792        })
793    }
794
795    /// Get number of sections
796    pub fn sections_count(&self) -> Result<usize> {
797        self.parse_script_with(|script| script.sections().len())
798    }
799
800    /// Check if document has events section
801    pub fn has_events(&self) -> Result<bool> {
802        self.parse_script_with(|script| {
803            script
804                .sections()
805                .iter()
806                .any(|section| matches!(section, Section::Events(_)))
807        })
808    }
809
810    /// Check if document has styles section
811    pub fn has_styles(&self) -> Result<bool> {
812        self.parse_script_with(|script| {
813            script
814                .sections()
815                .iter()
816                .any(|section| matches!(section, Section::Styles(_)))
817        })
818    }
819
820    /// Get event text by line pattern (simplified search)
821    pub fn find_event_text(&self, pattern: &str) -> Result<Vec<String>> {
822        self.parse_script_with(|script| {
823            let mut matches = Vec::new();
824            for section in script.sections() {
825                if let Section::Events(events) = section {
826                    for event in events {
827                        if event.text.contains(pattern) {
828                            matches.push(event.text.to_string());
829                        }
830                    }
831                }
832            }
833            matches
834        })
835    }
836
837    /// Edit event by index with full field support
838    ///
839    /// Allows structured editing of specific event fields by index.
840    /// Returns the modified event line for undo support.
841    ///
842    /// # Arguments
843    ///
844    /// * `index` - Zero-based index of the event to edit
845    /// * `update_fn` - Function that receives the current event and returns modifications
846    ///
847    /// # Example
848    ///
849    /// ```rust
850    /// # use ass_editor::core::EditorDocument;
851    /// # let content = r#"[Script Info]
852    /// # Title: Test
853    /// #
854    /// # [Events]
855    /// # Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
856    /// # Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Original text"#;
857    /// # let mut doc = EditorDocument::from_content(content).unwrap();
858    /// doc.edit_event_by_index(0, |event| {
859    ///     vec![
860    ///         ("text", "New dialogue text".to_string()),
861    ///         ("style", "NewStyle".to_string()),
862    ///     ]
863    /// })?;
864    /// # Ok::<(), Box<dyn std::error::Error>>(())
865    /// ```
866    pub fn edit_event_by_index<F>(&mut self, index: usize, update_fn: F) -> Result<String>
867    where
868        F: for<'a> FnOnce(&ass_core::parser::ast::Event<'a>) -> Vec<(&'static str, String)>,
869    {
870        let content = self.text();
871        let mut event_info = None;
872        let mut event_count = 0;
873
874        // Find the event and its location in the document
875        self.parse_script_with(|script| -> Result<()> {
876            for section in script.sections() {
877                if let Section::Events(events) = section {
878                    for event in events {
879                        if event_count == index {
880                            // Get the modifications from the update function
881                            let modifications = update_fn(event);
882
883                            // Build a pattern to search for this specific event
884                            let event_type_str = event.event_type.as_str();
885                            let pattern = format!(
886                                "{}: {},{},{}",
887                                event_type_str, event.layer, event.start, event.end
888                            );
889
890                            let event_line = if let Some(pos) = content.find(&pattern) {
891                                let line_end = content[pos..]
892                                    .find('\n')
893                                    .map(|n| pos + n)
894                                    .unwrap_or(content.len());
895                                let line = content[pos..line_end].to_string();
896                                (pos, line_end, line)
897                            } else {
898                                return Err(EditorError::ValidationError {
899                                    message: "Could not find event line in document".to_string(),
900                                });
901                            };
902
903                            // Store the event data we need instead of cloning
904                            let event_data = (
905                                event.event_type,
906                                event.layer.to_string(),
907                                event.start.to_string(),
908                                event.end.to_string(),
909                                event.style.to_string(),
910                                event.name.to_string(),
911                                event.margin_l.to_string(),
912                                event.margin_r.to_string(),
913                                event.margin_v.to_string(),
914                                event.effect.to_string(),
915                                event.text.to_string(),
916                            );
917                            event_info = Some((event_data, event_line, modifications));
918                            return Ok(());
919                        }
920                        event_count += 1;
921                    }
922                }
923            }
924            Ok(())
925        })??;
926
927        if let Some((event_data, (line_start, line_end, original_line), modifications)) = event_info
928        {
929            // Build the new event line with modifications
930            let new_line = self.build_modified_event_line_from_data(
931                event_data,
932                &original_line,
933                modifications,
934            )?;
935
936            // Replace the line in the document
937            let range = Range::new(Position::new(line_start), Position::new(line_end));
938            self.replace(range, &new_line)?;
939
940            Ok(new_line)
941        } else {
942            Err(EditorError::InvalidRange {
943                start: index,
944                end: index + 1,
945                length: event_count,
946            })
947        }
948    }
949
950    /// Helper to build a modified event line from event data
951    fn build_modified_event_line_from_data(
952        &self,
953        event_data: (
954            ass_core::parser::ast::EventType,
955            String,
956            String,
957            String,
958            String,
959            String,
960            String,
961            String,
962            String,
963            String,
964            String,
965        ),
966        _original_line: &str,
967        modifications: Vec<(&'static str, String)>,
968    ) -> Result<String> {
969        let (
970            event_type,
971            layer,
972            start,
973            end,
974            style,
975            name,
976            margin_l,
977            margin_r,
978            margin_v,
979            effect,
980            text,
981        ) = event_data;
982
983        // Apply modifications
984        let mut layer = layer;
985        let mut start = start;
986        let mut end = end;
987        let mut style = style;
988        let mut name = name;
989        let mut margin_l = margin_l;
990        let mut margin_r = margin_r;
991        let mut margin_v = margin_v;
992        let mut effect = effect;
993        let mut text = text;
994
995        for (field, value) in modifications {
996            match field {
997                "layer" => layer = value,
998                "start" => start = value,
999                "end" => end = value,
1000                "style" => style = value,
1001                "name" => name = value,
1002                "margin_l" => margin_l = value,
1003                "margin_r" => margin_r = value,
1004                "margin_v" => margin_v = value,
1005                "effect" => effect = value,
1006                "text" => text = value,
1007                _ => {
1008                    return Err(EditorError::ValidationError {
1009                        message: format!("Unknown event field: {field}"),
1010                    });
1011                }
1012            }
1013        }
1014
1015        // Rebuild the line
1016        let event_type_str = event_type.as_str();
1017        Ok(format!("{event_type_str}: {layer},{start},{end},{style},{name},{margin_l},{margin_r},{margin_v},{effect},{text}"))
1018    }
1019
1020    /// Add event line to document
1021    pub fn add_event_line(&mut self, event_line: &str) -> Result<()> {
1022        let content = self.text();
1023        if let Some(events_pos) = content.find("[Events]") {
1024            // Find end of format line and add after it
1025            let format_start = content[events_pos..].find("Format:").unwrap_or(0) + events_pos;
1026            let line_end = content[format_start..].find('\n').unwrap_or(0) + format_start + 1;
1027
1028            let insert_pos = Position::new(line_end);
1029            self.insert(insert_pos, &format!("{event_line}\n"))
1030        } else {
1031            // Add Events section if it doesn't exist
1032            let content_len = self.len_bytes();
1033            let events_section = format!("\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n{event_line}\n");
1034            self.insert(Position::new(content_len), &events_section)
1035        }
1036    }
1037
1038    /// Edit style line
1039    pub fn edit_style_line(&mut self, style_name: &str, new_style_line: &str) -> Result<()> {
1040        let content = self.text();
1041        let pattern = format!("Style: {style_name},");
1042
1043        if let Some(pos) = content.find(&pattern) {
1044            // Find end of line
1045            let line_end = content[pos..].find('\n').map_or(content.len(), |n| pos + n);
1046            let range = Range::new(Position::new(pos), Position::new(line_end));
1047            self.replace(range, new_style_line)
1048        } else {
1049            // Add style if it doesn't exist
1050            self.add_style_line(new_style_line)
1051        }
1052    }
1053
1054    /// Add style line to document
1055    pub fn add_style_line(&mut self, style_line: &str) -> Result<()> {
1056        let content = self.text();
1057        if let Some(styles_pos) = content
1058            .find("[V4+ Styles]")
1059            .or_else(|| content.find("[V4 Styles]"))
1060        {
1061            // Find end of format line and add after it
1062            let format_start = content[styles_pos..].find("Format:").unwrap_or(0) + styles_pos;
1063            let line_end = content[format_start..].find('\n').unwrap_or(0) + format_start + 1;
1064
1065            let insert_pos = Position::new(line_end);
1066            self.insert(insert_pos, &format!("{style_line}\n"))
1067        } else {
1068            // Add Styles section if it doesn't exist
1069            let script_info_end = content.find("\n[Events]").unwrap_or(content.len());
1070            let styles_section = format!("\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n{style_line}\n");
1071            self.insert(Position::new(script_info_end), &styles_section)
1072        }
1073    }
1074
1075    // === INCREMENTAL PARSING WITH CORE INTEGRATION ===
1076
1077    /// Perform incremental edit using core's parse_partial for optimal performance
1078    ///
1079    /// Includes error recovery with fallback strategies:
1080    /// 1. Try incremental parsing with Script::parse_partial()
1081    /// 2. On failure, fall back to full reparse
1082    /// 3. On repeated failures, reset parser state and retry
1083    #[cfg(feature = "stream")]
1084    pub fn edit_incremental(&mut self, range: Range, new_text: &str) -> Result<ScriptDeltaOwned> {
1085        use crate::core::history::Operation;
1086
1087        // Fast path for simple edits that don't require full parsing
1088        let is_simple_edit = new_text.len() <= 100 && // Small to medium edits
1089            !new_text.contains('[') && // No new sections
1090            new_text.matches('\n').count() <= 1 && // At most one line break
1091            range.len() <= 50; // Small replacements
1092
1093        if is_simple_edit {
1094            return self.edit_fast_path(range, new_text);
1095        }
1096
1097        // Get the old text for undo data
1098        #[cfg(feature = "std")]
1099        let old_text = self.text_range(range)?;
1100        #[cfg(not(feature = "std"))]
1101        let _old_text = self.text_range(range)?;
1102
1103        // Apply change with incremental parsing (includes fallback to full parse)
1104        let current_text = self.text();
1105        let delta = match self
1106            .incremental_parser
1107            .apply_change(&current_text, range, new_text)
1108        {
1109            Ok(delta) => delta,
1110            Err(_e) => {
1111                // Log the error for debugging
1112                #[cfg(feature = "std")]
1113                eprintln!("Incremental parsing failed, attempting recovery: {_e}");
1114
1115                // If incremental parsing fails repeatedly, reset the parser
1116                if self.incremental_parser.should_reparse() {
1117                    self.incremental_parser.clear_cache();
1118                }
1119
1120                // Try one more time with a fresh parser state
1121                match self
1122                    .incremental_parser
1123                    .apply_change(&current_text, range, new_text)
1124                {
1125                    Ok(delta) => delta,
1126                    Err(_) => {
1127                        // Final fallback: return a minimal delta indicating the change
1128                        ScriptDeltaOwned {
1129                            added: Vec::new(),
1130                            modified: vec![(0, "Script modified".to_string())],
1131                            removed: Vec::new(),
1132                            new_issues: Vec::new(),
1133                        }
1134                    }
1135                }
1136            }
1137        };
1138
1139        // Create undo data from the delta (must be captured BEFORE applying changes)
1140        let undo_data = self.capture_delta_undo_data(&delta)?;
1141
1142        // Create delta operation for history
1143        let operation = Operation::Delta {
1144            forward: delta.clone(),
1145            undo_data,
1146        };
1147
1148        // Create command result
1149        let result = CommandResult::success_with_change(
1150            range,
1151            Position::new(range.start.offset + new_text.len()),
1152        );
1153
1154        // Record in history
1155        let result_with_delta = result.with_delta(delta.clone());
1156        self.history.record_operation(
1157            operation,
1158            format!("Incremental edit at {}", range.start.offset),
1159            &result_with_delta,
1160        );
1161
1162        // Apply the text change
1163        self.replace_raw(range, new_text)?;
1164
1165        // Mark as modified
1166        self.modified = true;
1167
1168        // Emit event
1169        #[cfg(feature = "std")]
1170        self.emit(DocumentEvent::TextReplaced {
1171            range,
1172            old_text,
1173            new_text: new_text.to_string(),
1174        });
1175
1176        Ok(delta)
1177    }
1178
1179    /// Insert text with incremental parsing (< 1ms target)
1180    #[cfg(feature = "stream")]
1181    pub fn insert_incremental(&mut self, pos: Position, text: &str) -> Result<ScriptDeltaOwned> {
1182        let range = Range::new(pos, pos); // Zero-length range for insertion
1183        self.edit_incremental(range, text)
1184    }
1185
1186    /// Delete text with incremental parsing
1187    #[cfg(feature = "stream")]
1188    pub fn delete_incremental(&mut self, range: Range) -> Result<ScriptDeltaOwned> {
1189        self.edit_incremental(range, "")
1190    }
1191
1192    /// Fast path for simple edits that avoids heavy parsing
1193    #[cfg(feature = "stream")]
1194    fn edit_fast_path(&mut self, range: Range, new_text: &str) -> Result<ScriptDeltaOwned> {
1195        use crate::core::history::Operation;
1196
1197        // Validate that range boundaries are on valid UTF-8 char boundaries
1198        let text = self.text();
1199        if !text.is_char_boundary(range.start.offset) || !text.is_char_boundary(range.end.offset) {
1200            // Fall back to regular incremental parsing for invalid boundaries
1201            return self.edit_incremental_fallback(range, new_text);
1202        }
1203
1204        // Get old text for undo
1205        let old_text = if range.is_empty() {
1206            String::new()
1207        } else {
1208            self.text_range(range)?
1209        };
1210
1211        // Simple text replacement without parsing
1212        self.replace_raw(range, new_text)?;
1213
1214        // Create minimal undo operation
1215        let operation = Operation::Replace {
1216            range,
1217            new_text: new_text.to_string(),
1218            old_text,
1219        };
1220
1221        // Create result
1222        let result = CommandResult::success_with_change(
1223            range,
1224            Position::new(range.start.offset + new_text.len()),
1225        );
1226
1227        // Record in history (without delta)
1228        self.history
1229            .record_operation(operation, "Fast character insert".to_string(), &result);
1230
1231        // Mark as modified
1232        self.modified = true;
1233
1234        // Return minimal delta
1235        Ok(ScriptDeltaOwned {
1236            added: Vec::new(),
1237            modified: Vec::new(),
1238            removed: Vec::new(),
1239            new_issues: Vec::new(),
1240        })
1241    }
1242
1243    /// Fallback for edit_incremental that avoids infinite recursion
1244    #[cfg(feature = "stream")]
1245    fn edit_incremental_fallback(
1246        &mut self,
1247        range: Range,
1248        new_text: &str,
1249    ) -> Result<ScriptDeltaOwned> {
1250        // Just do a simple replace without the fast path
1251        self.replace(range, new_text)?;
1252
1253        // Return minimal delta
1254        Ok(ScriptDeltaOwned {
1255            added: Vec::new(),
1256            modified: Vec::new(),
1257            removed: Vec::new(),
1258            new_issues: Vec::new(),
1259        })
1260    }
1261
1262    /// Safe edit with automatic fallback to regular replace on error
1263    ///
1264    /// This method tries incremental parsing first for performance,
1265    /// but falls back to regular replace if incremental parsing is unavailable
1266    /// or fails. This ensures edits always succeed.
1267    pub fn edit_safe(&mut self, range: Range, new_text: &str) -> Result<()> {
1268        #[cfg(feature = "stream")]
1269        {
1270            // Try incremental parsing first
1271            match self.edit_incremental(range, new_text) {
1272                Ok(_) => return Ok(()),
1273                Err(_e) => {
1274                    #[cfg(feature = "std")]
1275                    eprintln!("Incremental edit failed, falling back to regular replace: {_e}");
1276                }
1277            }
1278        }
1279
1280        // Fallback to regular replace
1281        self.replace(range, new_text)
1282    }
1283
1284    /// Edit event using incremental parsing for performance
1285    #[cfg(feature = "stream")]
1286    pub fn edit_event_incremental(
1287        &mut self,
1288        event_text: &str,
1289        new_text: &str,
1290    ) -> Result<ScriptDeltaOwned> {
1291        let content = self.text();
1292        if let Some(pos) = content.find(event_text) {
1293            let range = Range::new(Position::new(pos), Position::new(pos + event_text.len()));
1294            self.edit_incremental(range, new_text)
1295        } else {
1296            Err(EditorError::ValidationError {
1297                message: format!("Event text not found: {event_text}"),
1298            })
1299        }
1300    }
1301
1302    /// Parse with delta tracking for command system integration
1303    #[cfg(feature = "stream")]
1304    pub fn parse_with_delta_tracking<F, R>(
1305        &self,
1306        range: Option<StdRange<usize>>,
1307        new_text: Option<&str>,
1308        f: F,
1309    ) -> Result<R>
1310    where
1311        F: FnOnce(&Script, Option<&ScriptDeltaOwned>) -> R,
1312    {
1313        let content = self.text();
1314        let script = Script::parse(&content).map_err(EditorError::from)?;
1315
1316        if let (Some(range), Some(text)) = (range, new_text) {
1317            // Get delta for the change
1318            match script.parse_partial(range, text) {
1319                Ok(delta) => Ok(f(&script, Some(&delta))),
1320                Err(_) => {
1321                    // Fallback to full re-parse if incremental fails
1322                    Ok(f(&script, None))
1323                }
1324            }
1325        } else {
1326            Ok(f(&script, None))
1327        }
1328    }
1329
1330    /// Edit event using a builder for structured modifications
1331    ///
1332    /// Allows editing events using the EventBuilder fluent API. The builder
1333    /// is pre-populated with the current event's values, allowing selective
1334    /// field updates.
1335    ///
1336    /// # Arguments
1337    ///
1338    /// * `index` - Zero-based index of the event to edit  
1339    /// * `builder_fn` - Function that receives a pre-populated EventBuilder
1340    ///
1341    /// # Example
1342    ///
1343    /// ```rust
1344    /// # use ass_editor::core::EditorDocument;
1345    /// # let content = r#"[Script Info]
1346    /// # Title: Test
1347    /// #
1348    /// # [Events]
1349    /// # Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
1350    /// # Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Original text"#;
1351    /// # let mut doc = EditorDocument::from_content(content).unwrap();
1352    /// # use ass_editor::core::builders::EventBuilder;
1353    /// doc.edit_event_with_builder(0, |builder| {
1354    ///     builder
1355    ///         .text("New dialogue text")
1356    ///         .style("NewStyle")
1357    ///         .end_time("0:00:10.00")
1358    /// })?;
1359    /// # Ok::<(), Box<dyn std::error::Error>>(())
1360    /// ```
1361    pub fn edit_event_with_builder<F>(&mut self, index: usize, builder_fn: F) -> Result<String>
1362    where
1363        F: for<'a> FnOnce(
1364            crate::core::builders::EventBuilder,
1365        ) -> crate::core::builders::EventBuilder,
1366    {
1367        use crate::core::builders::EventBuilder;
1368
1369        let content = self.text();
1370        let mut event_info = None;
1371        let mut event_count = 0;
1372        let mut format_line = None;
1373
1374        // Find the event and extract format line
1375        self.parse_script_with(|script| -> Result<()> {
1376            for section in script.sections() {
1377                if let Section::Events(events) = section {
1378                    // Get format line if available
1379                    if format_line.is_none() {
1380                        // Find Events section header and format line in raw text
1381                        if let Some(events_pos) = content.find("[Events]") {
1382                            let after_header = &content[events_pos + 8..];
1383                            if let Some(format_pos) = after_header.find("Format:") {
1384                                let format_start = events_pos + 8 + format_pos + 7; // Skip "Format:"
1385                                if let Some(format_end) = content[format_start..].find('\n') {
1386                                    let format_str =
1387                                        content[format_start..format_start + format_end].trim();
1388                                    let fields: Vec<&str> =
1389                                        format_str.split(',').map(str::trim).collect();
1390                                    format_line = Some(fields);
1391                                }
1392                            }
1393                        }
1394                    }
1395
1396                    for event in events {
1397                        if event_count == index {
1398                            // Create a builder pre-populated with current values
1399                            let mut builder = match event.event_type {
1400                                ass_core::parser::ast::EventType::Dialogue => {
1401                                    EventBuilder::dialogue()
1402                                }
1403                                ass_core::parser::ast::EventType::Comment => {
1404                                    EventBuilder::comment()
1405                                }
1406                                _ => EventBuilder::new(),
1407                            };
1408
1409                            // Pre-populate builder with current event values
1410                            builder = builder
1411                                .layer(event.layer.parse::<u32>().unwrap_or(0))
1412                                .start_time(event.start)
1413                                .end_time(event.end)
1414                                .style(event.style)
1415                                .speaker(event.name)
1416                                .margin_left(event.margin_l.parse::<u32>().unwrap_or(0))
1417                                .margin_right(event.margin_r.parse::<u32>().unwrap_or(0))
1418                                .margin_vertical(event.margin_v.parse::<u32>().unwrap_or(0))
1419                                .effect(event.effect)
1420                                .text(event.text);
1421
1422                            if let Some(margin_t) = event.margin_t {
1423                                builder = builder.margin_top(margin_t.parse::<u32>().unwrap_or(0));
1424                            }
1425                            if let Some(margin_b) = event.margin_b {
1426                                builder =
1427                                    builder.margin_bottom(margin_b.parse::<u32>().unwrap_or(0));
1428                            }
1429
1430                            // Apply user modifications
1431                            let modified_builder = builder_fn(builder);
1432
1433                            // Build the new event line
1434                            let new_line = if let Some(ref format_fields) = format_line {
1435                                modified_builder.build_with_format(format_fields)?
1436                            } else {
1437                                modified_builder.build()?
1438                            };
1439
1440                            // Find the event line in the raw text
1441                            let event_type_str = event.event_type.as_str();
1442                            let pattern = format!(
1443                                "{}: {},{},{}",
1444                                event_type_str, event.layer, event.start, event.end
1445                            );
1446
1447                            let event_line = if let Some(pos) = content.find(&pattern) {
1448                                let line_end = content[pos..]
1449                                    .find('\n')
1450                                    .map(|n| pos + n)
1451                                    .unwrap_or(content.len());
1452                                (pos, line_end)
1453                            } else {
1454                                return Err(EditorError::ValidationError {
1455                                    message: "Could not find event line in document".to_string(),
1456                                });
1457                            };
1458
1459                            event_info = Some((event_line, new_line));
1460                            return Ok(());
1461                        }
1462                        event_count += 1;
1463                    }
1464                }
1465            }
1466            Ok(())
1467        })??;
1468
1469        if let Some(((line_start, line_end), new_line)) = event_info {
1470            // Replace the line in the document
1471            let range = Range::new(Position::new(line_start), Position::new(line_end));
1472            self.replace(range, &new_line)?;
1473
1474            Ok(new_line)
1475        } else {
1476            Err(EditorError::InvalidRange {
1477                start: index,
1478                end: index + 1,
1479                length: event_count,
1480            })
1481        }
1482    }
1483
1484    /// Edit an event by finding and replacing text (simplified ASS-aware editing)
1485    pub fn edit_event_text(&mut self, old_text: &str, new_text: &str) -> Result<()> {
1486        let content = self.text();
1487
1488        if let Some(pos) = content.find(old_text) {
1489            let range = Range::new(Position::new(pos), Position::new(pos + old_text.len()));
1490            self.replace(range, new_text)?;
1491        }
1492
1493        Ok(())
1494    }
1495
1496    /// Get script info field value by key
1497    pub fn get_script_info_field(&self, key: &str) -> Result<Option<String>> {
1498        self.parse_script_with(|script| {
1499            script.sections().iter().find_map(|section| {
1500                if let Section::ScriptInfo(info) = section {
1501                    info.fields
1502                        .iter()
1503                        .find(|(k, _)| *k == key)
1504                        .map(|(_, v)| v.to_string())
1505                } else {
1506                    None
1507                }
1508            })
1509        })
1510    }
1511
1512    /// Set script info field (ASS-aware editing)
1513    pub fn set_script_info_field(&mut self, key: &str, value: &str) -> Result<()> {
1514        // Simplified implementation - find and replace the field
1515        let content = self.text();
1516        let field_pattern = format!("{key}:");
1517
1518        if let Some(pos) = content.find(&field_pattern) {
1519            // Find end of line
1520            let line_start = pos;
1521            let line_end = content[pos..].find('\n').map_or(content.len(), |n| pos + n);
1522
1523            let range = Range::new(Position::new(line_start), Position::new(line_end));
1524
1525            let new_line = format!("{key}: {value}");
1526            self.replace(range, &new_line)?;
1527        }
1528
1529        Ok(())
1530    }
1531
1532    /// Perform an undo operation
1533    ///
1534    /// Retrieves the most recent operation from the undo stack and reverses it.
1535    /// If the operation includes a script delta, it will be applied for efficient updates.
1536    pub fn undo(&mut self) -> Result<CommandResult> {
1537        use crate::core::history::Operation;
1538
1539        // Pop from undo stack
1540        if let Some(entry) = self.history.pop_undo_entry() {
1541            let mut result = CommandResult::success();
1542            result.content_changed = true;
1543
1544            // Execute the inverse of the operation
1545            match &entry.operation {
1546                Operation::Insert { position, text } => {
1547                    // Undo insert by deleting the inserted text
1548                    let end_pos = Position::new(position.offset + text.len());
1549                    let range = Range::new(*position, end_pos);
1550                    self.delete_raw(range)?;
1551                    result.modified_range = Some(Range::new(*position, *position));
1552                    result.new_cursor = entry.cursor_before;
1553                }
1554                Operation::Delete {
1555                    range,
1556                    deleted_text,
1557                } => {
1558                    // Undo delete by inserting the deleted text
1559                    self.insert_raw(range.start, deleted_text)?;
1560                    let end_pos = Position::new(range.start.offset + deleted_text.len());
1561                    result.modified_range = Some(Range::new(range.start, end_pos));
1562                    result.new_cursor = entry.cursor_before;
1563                }
1564                Operation::Replace {
1565                    range, old_text, ..
1566                } => {
1567                    // Undo replace by restoring old text
1568                    self.replace_raw(*range, old_text)?;
1569                    let end_pos = Position::new(range.start.offset + old_text.len());
1570                    result.modified_range = Some(Range::new(range.start, end_pos));
1571                    result.new_cursor = entry.cursor_before;
1572                }
1573                #[cfg(feature = "stream")]
1574                Operation::Delta { forward, undo_data } => {
1575                    // Restore removed sections
1576                    for (index, section_text) in undo_data.removed_sections.iter() {
1577                        self.insert_section_at(*index, section_text)?;
1578                    }
1579
1580                    // Restore modified sections
1581                    for (index, original_text) in undo_data.modified_sections.iter() {
1582                        self.replace_section(*index, original_text)?;
1583                    }
1584
1585                    // Remove added sections
1586                    for _ in 0..forward.added.len() {
1587                        self.remove_last_section()?;
1588                    }
1589
1590                    result.message = Some("Delta operation undone".to_string());
1591                }
1592            }
1593
1594            // Push to redo stack for future redo
1595            self.history.push_redo_entry(entry);
1596
1597            // Apply script delta if available
1598            #[cfg(feature = "stream")]
1599            if let Some(delta) = result.script_delta.as_ref() {
1600                self.apply_script_delta(delta.clone())?;
1601            }
1602
1603            result.message = Some("Undo successful".to_string());
1604            Ok(result)
1605        } else {
1606            Err(EditorError::NothingToUndo)
1607        }
1608    }
1609
1610    /// Perform a redo operation
1611    ///
1612    /// Retrieves the most recent operation from the redo stack and re-executes it.
1613    /// If the operation includes a script delta, it will be applied for efficient updates.
1614    pub fn redo(&mut self) -> Result<CommandResult> {
1615        use crate::core::history::Operation;
1616
1617        // Pop from redo stack
1618        if let Some(entry) = self.history.pop_redo_entry() {
1619            let mut result = CommandResult::success();
1620            result.content_changed = true;
1621
1622            // Re-execute the original operation
1623            match &entry.operation {
1624                Operation::Insert { position, text } => {
1625                    // Redo insert
1626                    self.insert_raw(*position, text)?;
1627                    let end_pos = Position::new(position.offset + text.len());
1628                    result.modified_range = Some(Range::new(*position, end_pos));
1629                    result.new_cursor = entry.cursor_after;
1630                }
1631                Operation::Delete { range, .. } => {
1632                    // Redo delete
1633                    self.delete_raw(*range)?;
1634                    result.modified_range = Some(Range::new(range.start, range.start));
1635                    result.new_cursor = entry.cursor_after;
1636                }
1637                Operation::Replace {
1638                    range, new_text, ..
1639                } => {
1640                    // Redo replace
1641                    self.replace_raw(*range, new_text)?;
1642                    let end_pos = Position::new(range.start.offset + new_text.len());
1643                    result.modified_range = Some(Range::new(range.start, end_pos));
1644                    result.new_cursor = entry.cursor_after;
1645                }
1646                #[cfg(feature = "stream")]
1647                Operation::Delta {
1648                    forward,
1649                    undo_data: _,
1650                } => {
1651                    // Re-apply the delta
1652                    self.apply_script_delta(forward.clone())?;
1653                    result.message = Some("Delta re-applied".to_string());
1654                }
1655            }
1656
1657            // Record in history without using the public methods (to avoid recursion)
1658            // We need to manually update the history manager's cursor
1659            if let Some(cursor) = result.new_cursor {
1660                self.history.set_cursor(Some(cursor));
1661            }
1662
1663            // Create a new history entry for the redo operation
1664            let new_entry = crate::core::history::HistoryEntry::new(
1665                entry.operation,
1666                entry.description,
1667                &result,
1668                entry.cursor_before,
1669            );
1670
1671            // Push back to undo stack
1672            self.history.stack_mut().push(new_entry);
1673
1674            result.message = Some("Redo successful".to_string());
1675            Ok(result)
1676        } else {
1677            Err(EditorError::NothingToRedo)
1678        }
1679    }
1680
1681    /// Check if undo is available
1682    pub fn can_undo(&self) -> bool {
1683        self.history.can_undo()
1684    }
1685
1686    /// Check if redo is available
1687    pub fn can_redo(&self) -> bool {
1688        self.history.can_redo()
1689    }
1690
1691    /// Get description of the next undo operation
1692    pub fn next_undo_description(&self) -> Option<&str> {
1693        self.history.next_undo_description()
1694    }
1695
1696    /// Get description of the next redo operation
1697    pub fn next_redo_description(&self) -> Option<&str> {
1698        self.history.next_redo_description()
1699    }
1700
1701    /// Get mutable reference to the undo manager for configuration
1702    pub fn undo_manager_mut(&mut self) -> &mut UndoManager {
1703        &mut self.history
1704    }
1705
1706    /// Get reference to the undo manager
1707    pub fn undo_manager(&self) -> &UndoManager {
1708        &self.history
1709    }
1710
1711    /// Apply a script delta and record it with undo data
1712    #[cfg(feature = "stream")]
1713    pub fn apply_script_delta(&mut self, delta: ScriptDeltaOwned) -> Result<()> {
1714        use crate::core::history::Operation;
1715
1716        // Capture undo data before applying
1717        let undo_data = self.capture_delta_undo_data(&delta)?;
1718
1719        // Apply delta
1720        self.apply_script_delta_internal(delta.clone())?;
1721
1722        // Record with undo data
1723        let operation = Operation::Delta {
1724            forward: delta,
1725            undo_data,
1726        };
1727
1728        let result = CommandResult::success();
1729        self.history
1730            .record_operation(operation, "Apply delta".to_string(), &result);
1731
1732        Ok(())
1733    }
1734
1735    /// Capture undo data before applying a delta
1736    #[cfg(feature = "stream")]
1737    fn capture_delta_undo_data(
1738        &self,
1739        delta: &ScriptDeltaOwned,
1740    ) -> Result<crate::core::history::DeltaUndoData> {
1741        let mut removed_sections = Vec::new();
1742        let mut modified_sections = Vec::new();
1743
1744        // First, get the current content for section extraction
1745        let content = self.text();
1746
1747        // For incremental edits, use simplified undo data to avoid expensive full parsing
1748        // Only do detailed section analysis for larger operations
1749        #[cfg(feature = "stream")]
1750        let is_small_edit = delta.added.len() + delta.removed.len() + delta.modified.len() <= 2;
1751        #[cfg(not(feature = "stream"))]
1752        let is_small_edit = false;
1753
1754        if is_small_edit {
1755            // For small incremental edits, skip expensive undo data capture
1756            // The basic text-based undo in edit_incremental will handle this
1757        } else {
1758            // For larger operations, do full undo data capture
1759            let _ = self.parse_script_with(|script| {
1760                // Capture removed sections
1761                for &index in &delta.removed {
1762                    if let Some(section) = script.sections().get(index) {
1763                        match self.extract_section_text(&content, section) {
1764                            Ok(section_text) => removed_sections.push((index, section_text)),
1765                            Err(_) => {
1766                                // If we can't extract the section text, store a placeholder
1767                                removed_sections.push((index, String::new()));
1768                            }
1769                        }
1770                    }
1771                }
1772
1773                // Capture original state of modified sections
1774                for (index, _) in &delta.modified {
1775                    if let Some(section) = script.sections().get(*index) {
1776                        match self.extract_section_text(&content, section) {
1777                            Ok(section_text) => modified_sections.push((*index, section_text)),
1778                            Err(_) => {
1779                                // If we can't extract the section text, store a placeholder
1780                                modified_sections.push((*index, String::new()));
1781                            }
1782                        }
1783                    }
1784                }
1785
1786                Ok::<(), EditorError>(())
1787            })?;
1788        }
1789
1790        Ok(crate::core::history::DeltaUndoData {
1791            removed_sections,
1792            modified_sections,
1793        })
1794    }
1795
1796    /// Extract section text from content
1797    #[cfg(feature = "stream")]
1798    fn extract_section_text(&self, content: &str, section: &Section) -> Result<String> {
1799        let header = match section {
1800            Section::ScriptInfo(_) => "[Script Info]",
1801            Section::Styles(_) => "[V4+ Styles]",
1802            Section::Events(_) => "[Events]",
1803            Section::Fonts(_) => "[Fonts]",
1804            Section::Graphics(_) => "[Graphics]",
1805        };
1806
1807        // Find the header in the content
1808        let start = content
1809            .find(header)
1810            .ok_or_else(|| EditorError::SectionNotFound {
1811                section: header.to_string(),
1812            })?;
1813
1814        // Find the next section header or end of document
1815        let section_headers = [
1816            "[Script Info]",
1817            "[V4+ Styles]",
1818            "[Events]",
1819            "[Fonts]",
1820            "[Graphics]",
1821        ];
1822
1823        let end = content[start + header.len()..]
1824            .find(|_c: char| {
1825                for sh in &section_headers {
1826                    if content[start + header.len()..].starts_with(sh) {
1827                        return true;
1828                    }
1829                }
1830                false
1831            })
1832            .map(|pos| start + header.len() + pos)
1833            .unwrap_or(content.len());
1834
1835        Ok(content[start..end].to_string())
1836    }
1837
1838    /// Apply a script delta for efficient incremental parsing (internal)
1839    #[cfg(feature = "stream")]
1840    fn apply_script_delta_internal(&mut self, delta: ScriptDeltaOwned) -> Result<()> {
1841        // Parse the current script to get sections
1842        let current_content = self.text();
1843        let script = Script::parse(&current_content).map_err(EditorError::from)?;
1844
1845        // Apply removals first (in reverse order to maintain indices)
1846        let mut removed_indices = delta.removed.clone();
1847        removed_indices.sort_by(|a, b| b.cmp(a)); // Sort descending
1848
1849        for index in removed_indices {
1850            if index < script.sections().len() {
1851                // Find the section's text range and remove it
1852                let section = &script.sections()[index];
1853                let start_offset = self.find_section_start(section)?;
1854                let end_offset = self.find_section_end(section)?;
1855
1856                self.delete_raw(Range::new(
1857                    Position::new(start_offset),
1858                    Position::new(end_offset),
1859                ))?;
1860            }
1861        }
1862
1863        // Apply modifications
1864        for (index, new_section_text) in delta.modified {
1865            if index < script.sections().len() {
1866                // Find the section's text range
1867                let section = &script.sections()[index];
1868                let start_offset = self.find_section_start(section)?;
1869                let end_offset = self.find_section_end(section)?;
1870
1871                // Replace with new section text
1872                self.replace_raw(
1873                    Range::new(Position::new(start_offset), Position::new(end_offset)),
1874                    &new_section_text,
1875                )?;
1876            }
1877        }
1878
1879        // Apply additions
1880        for section_text in delta.added {
1881            // Add new sections at the end of the document
1882            let end_pos = Position::new(self.len_bytes());
1883
1884            // Ensure proper newline before new section
1885            if self.len_bytes() > 0 && !self.text().ends_with('\n') {
1886                self.insert_raw(end_pos, "\n")?;
1887            }
1888
1889            self.insert_raw(Position::new(self.len_bytes()), &section_text)?;
1890
1891            // Ensure trailing newline
1892            if !section_text.ends_with('\n') {
1893                self.insert_raw(Position::new(self.len_bytes()), "\n")?;
1894            }
1895        }
1896
1897        // Validate the result
1898        let _ = Script::parse(&self.text()).map_err(EditorError::from)?;
1899
1900        Ok(())
1901    }
1902
1903    /// Find the start offset of a section in the document
1904    #[cfg(feature = "stream")]
1905    fn find_section_start(&self, section: &Section) -> Result<usize> {
1906        // Get the section header text
1907        let header = match section {
1908            Section::ScriptInfo(_) => "[Script Info]",
1909            Section::Styles(_) => "[V4+ Styles]",
1910            Section::Events(_) => "[Events]",
1911            Section::Fonts(_) => "[Fonts]",
1912            Section::Graphics(_) => "[Graphics]",
1913        };
1914
1915        // Find the header in the document
1916        if let Some(pos) = self.text().find(header) {
1917            Ok(pos)
1918        } else {
1919            Err(EditorError::SectionNotFound {
1920                section: header.to_string(),
1921            })
1922        }
1923    }
1924
1925    /// Find the end offset of a section in the document
1926    #[cfg(feature = "stream")]
1927    fn find_section_end(&self, section: &Section) -> Result<usize> {
1928        let start = self.find_section_start(section)?;
1929        let content = &self.text()[start..];
1930
1931        // Find the next section header or end of document
1932        let section_headers = [
1933            "[Script Info]",
1934            "[V4+ Styles]",
1935            "[Events]",
1936            "[Fonts]",
1937            "[Graphics]",
1938        ];
1939
1940        let mut end_offset = content.len();
1941        for header in &section_headers {
1942            if let Some(pos) = content.find(header) {
1943                if pos > 0 {
1944                    end_offset = end_offset.min(pos);
1945                }
1946            }
1947        }
1948
1949        Ok(start + end_offset)
1950    }
1951
1952    /// Delete text in range (low-level operation without undo)
1953    pub(crate) fn delete_raw(&mut self, range: Range) -> Result<()> {
1954        if range.end.offset > self.len_bytes() {
1955            return Err(EditorError::InvalidRange {
1956                start: range.start.offset,
1957                end: range.end.offset,
1958                length: self.len_bytes(),
1959            });
1960        }
1961
1962        #[cfg(feature = "rope")]
1963        {
1964            // Convert byte offsets to char indices for rope operations
1965            let start_char = self.text_rope.byte_to_char(range.start.offset);
1966            let end_char = self.text_rope.byte_to_char(range.end.offset);
1967            self.text_rope.remove(start_char..end_char);
1968        }
1969        #[cfg(not(feature = "rope"))]
1970        {
1971            self.text_content
1972                .drain(range.start.offset..range.end.offset);
1973        }
1974
1975        self.modified = true;
1976        Ok(())
1977    }
1978
1979    /// Replace text in range (low-level operation without undo)
1980    pub(crate) fn replace_raw(&mut self, range: Range, text: &str) -> Result<()> {
1981        self.delete_raw(range)?;
1982        self.insert_raw(range.start, text)?;
1983        Ok(())
1984    }
1985
1986    // === Delta undo/redo helper methods ===
1987
1988    /// Insert a section at a specific index
1989    #[cfg(feature = "stream")]
1990    fn insert_section_at(&mut self, index: usize, section_text: &str) -> Result<()> {
1991        // Get the current sections count
1992        let section_count = self.parse_script_with(|script| script.sections().len())?;
1993
1994        // If index is beyond current sections, append to end
1995        if index >= section_count {
1996            let end_pos = Position::new(self.len_bytes());
1997
1998            // Ensure proper newline before new section
1999            if self.len_bytes() > 0 && !self.text().ends_with('\n') {
2000                self.insert_raw(end_pos, "\n")?;
2001            }
2002
2003            self.insert_raw(Position::new(self.len_bytes()), section_text)?;
2004
2005            // Ensure trailing newline
2006            if !section_text.ends_with('\n') {
2007                self.insert_raw(Position::new(self.len_bytes()), "\n")?;
2008            }
2009
2010            return Ok(());
2011        }
2012
2013        // Find the position where to insert the new section
2014        let content = self.text();
2015        let insert_pos = self.parse_script_with(|script| -> Result<usize> {
2016            if let Some(section) = script.sections().get(index) {
2017                // Find the start of this section to insert before it
2018                let header = match section {
2019                    Section::ScriptInfo(_) => "[Script Info]",
2020                    Section::Styles(_) => "[V4+ Styles]",
2021                    Section::Events(_) => "[Events]",
2022                    Section::Fonts(_) => "[Fonts]",
2023                    Section::Graphics(_) => "[Graphics]",
2024                };
2025
2026                if let Some(pos) = content.find(header) {
2027                    Ok(pos)
2028                } else {
2029                    Err(EditorError::SectionNotFound {
2030                        section: header.to_string(),
2031                    })
2032                }
2033            } else {
2034                // Append to end if index is out of bounds
2035                Ok(content.len())
2036            }
2037        })??;
2038
2039        // Insert the section at the found position
2040        let mut text_to_insert = section_text.to_string();
2041
2042        // Ensure section ends with newline
2043        if !text_to_insert.ends_with('\n') {
2044            text_to_insert.push('\n');
2045        }
2046
2047        // Add extra newline if needed to separate from next section
2048        if insert_pos < content.len() {
2049            text_to_insert.push('\n');
2050        }
2051
2052        self.insert_raw(Position::new(insert_pos), &text_to_insert)?;
2053
2054        Ok(())
2055    }
2056
2057    /// Replace a section at a specific index
2058    #[cfg(feature = "stream")]
2059    fn replace_section(&mut self, index: usize, new_text: &str) -> Result<()> {
2060        // Parse to find the section and get its boundaries
2061        // Get the section to replace
2062        let content = self.text();
2063        let section_info: Result<Option<&str>> = self.parse_script_with(|script| {
2064            if let Some(section) = script.sections().get(index) {
2065                let header = match section {
2066                    Section::ScriptInfo(_) => "[Script Info]",
2067                    Section::Styles(_) => "[V4+ Styles]",
2068                    Section::Events(_) => "[Events]",
2069                    Section::Fonts(_) => "[Fonts]",
2070                    Section::Graphics(_) => "[Graphics]",
2071                };
2072                Ok(Some(header))
2073            } else {
2074                Ok(None)
2075            }
2076        })?;
2077
2078        if let Some(header) = section_info? {
2079            let start = self.find_section_start_by_header(&content, header)?;
2080            let end = self.find_section_end_from_start(&content, start)?;
2081
2082            self.replace_raw(
2083                Range::new(Position::new(start), Position::new(end)),
2084                new_text,
2085            )?;
2086        }
2087
2088        Ok(())
2089    }
2090
2091    /// Remove the last section
2092    #[cfg(feature = "stream")]
2093    fn remove_last_section(&mut self) -> Result<()> {
2094        // Parse to find the last section and get its boundaries
2095        // Get the last section
2096        let content = self.text();
2097        let section_info: Result<Option<&str>> = self.parse_script_with(|script| {
2098            if let Some(section) = script.sections().last() {
2099                let header = match section {
2100                    Section::ScriptInfo(_) => "[Script Info]",
2101                    Section::Styles(_) => "[V4+ Styles]",
2102                    Section::Events(_) => "[Events]",
2103                    Section::Fonts(_) => "[Fonts]",
2104                    Section::Graphics(_) => "[Graphics]",
2105                };
2106                Ok(Some(header))
2107            } else {
2108                Ok(None)
2109            }
2110        })?;
2111
2112        if let Some(header) = section_info? {
2113            let start = self.find_section_start_by_header(&content, header)?;
2114            let end = self.find_section_end_from_start(&content, start)?;
2115
2116            self.delete_raw(Range::new(Position::new(start), Position::new(end)))?;
2117        }
2118
2119        Ok(())
2120    }
2121
2122    /// Find section start by header
2123    #[cfg(feature = "stream")]
2124    fn find_section_start_by_header(&self, content: &str, header: &str) -> Result<usize> {
2125        content
2126            .find(header)
2127            .ok_or_else(|| EditorError::SectionNotFound {
2128                section: header.to_string(),
2129            })
2130    }
2131
2132    /// Find section end from start position
2133    #[cfg(feature = "stream")]
2134    fn find_section_end_from_start(&self, content: &str, start: usize) -> Result<usize> {
2135        let section_headers = [
2136            "[Script Info]",
2137            "[V4+ Styles]",
2138            "[Events]",
2139            "[Fonts]",
2140            "[Graphics]",
2141        ];
2142
2143        // Find the next section header after start
2144        let mut end = content.len();
2145        for header in &section_headers {
2146            if let Some(pos) = content[start + 1..].find(header) {
2147                let actual_pos = start + 1 + pos;
2148                if actual_pos < end {
2149                    end = actual_pos;
2150                }
2151            }
2152        }
2153
2154        Ok(end)
2155    }
2156}
2157
2158impl Default for EditorDocument {
2159    fn default() -> Self {
2160        Self::new()
2161    }
2162}
2163
2164/// Fluent position API for editor operations
2165pub struct DocumentPosition<'a> {
2166    document: &'a mut EditorDocument,
2167    position: Position,
2168}
2169
2170impl<'a> DocumentPosition<'a> {
2171    /// Insert text at this position
2172    pub fn insert_text(self, text: &str) -> Result<()> {
2173        self.document.insert(self.position, text)
2174    }
2175
2176    /// Delete text range starting from this position
2177    pub fn delete_range(self, len: usize) -> Result<()> {
2178        let end_pos = Position::new(self.position.offset + len);
2179        let range = Range::new(self.position, end_pos);
2180        self.document.delete(range)
2181    }
2182
2183    /// Replace text at this position
2184    pub fn replace_text(self, len: usize, new_text: &str) -> Result<()> {
2185        let end_pos = Position::new(self.position.offset + len);
2186        let range = Range::new(self.position, end_pos);
2187        self.document.replace(range, new_text)
2188    }
2189}
2190
2191impl EditorDocument {
2192    /// Get fluent API for position-based operations
2193    pub fn at(&mut self, pos: Position) -> DocumentPosition {
2194        DocumentPosition {
2195            document: self,
2196            position: pos,
2197        }
2198    }
2199
2200    /// Initialize the extension registry with built-in handlers
2201    #[cfg(feature = "plugins")]
2202    pub fn initialize_registry(&mut self) -> Result<()> {
2203        use crate::extensions::registry_integration::RegistryIntegration;
2204
2205        let mut integration = RegistryIntegration::new();
2206
2207        // Register all built-in extensions using the function from the builtin module
2208        crate::extensions::builtin::register_builtin_extensions(&mut integration)?;
2209
2210        self.registry_integration = Some(Arc::new(integration));
2211        Ok(())
2212    }
2213
2214    /// Get the extension registry for use in parsing
2215    #[cfg(feature = "plugins")]
2216    pub fn registry(&self) -> Option<&ass_core::plugin::ExtensionRegistry> {
2217        self.registry_integration
2218            .as_ref()
2219            .map(|integration| integration.registry())
2220    }
2221
2222    /// Parse the document content with extension support and process it with a callback
2223    ///
2224    /// Since Script<'a> requires the source text to outlive it, this method uses a callback
2225    /// pattern to process the script while the content is still in scope.
2226    #[cfg(feature = "plugins")]
2227    pub fn parse_with_extensions<F, R>(&self, f: F) -> Result<R>
2228    where
2229        F: FnOnce(&ass_core::parser::Script) -> R,
2230    {
2231        let content = self.text();
2232
2233        if let Some(integration) = &self.registry_integration {
2234            // Parse with the extension registry using the builder pattern
2235            let script = ass_core::parser::Script::builder()
2236                .with_registry(integration.registry())
2237                .parse(&content)
2238                .map_err(EditorError::Core)?;
2239            Ok(f(&script))
2240        } else {
2241            // No registry, parse normally
2242            let script = ass_core::parser::Script::parse(&content).map_err(EditorError::Core)?;
2243            Ok(f(&script))
2244        }
2245    }
2246
2247    /// Register a custom tag handler
2248    #[cfg(feature = "plugins")]
2249    pub fn register_tag_handler(
2250        &mut self,
2251        extension_name: String,
2252        handler: Box<dyn ass_core::plugin::TagHandler>,
2253    ) -> Result<()> {
2254        if self.registry_integration.is_none() {
2255            self.initialize_registry()?;
2256        }
2257
2258        let registry_ref =
2259            self.registry_integration
2260                .as_mut()
2261                .ok_or_else(|| EditorError::ExtensionError {
2262                    extension: extension_name.clone(),
2263                    message: "Registry integration not available".to_string(),
2264                })?;
2265
2266        if let Some(integration) = Arc::get_mut(registry_ref) {
2267            integration.register_custom_tag_handler(extension_name, handler)
2268        } else {
2269            Err(EditorError::ExtensionError {
2270                extension: extension_name,
2271                message: "Cannot modify shared registry integration".to_string(),
2272            })
2273        }
2274    }
2275
2276    /// Register a custom section processor
2277    #[cfg(feature = "plugins")]
2278    pub fn register_section_processor(
2279        &mut self,
2280        extension_name: String,
2281        processor: Box<dyn ass_core::plugin::SectionProcessor>,
2282    ) -> Result<()> {
2283        if self.registry_integration.is_none() {
2284            self.initialize_registry()?;
2285        }
2286
2287        let registry_ref =
2288            self.registry_integration
2289                .as_mut()
2290                .ok_or_else(|| EditorError::ExtensionError {
2291                    extension: extension_name.clone(),
2292                    message: "Registry integration not available".to_string(),
2293                })?;
2294
2295        if let Some(integration) = Arc::get_mut(registry_ref) {
2296            integration.register_custom_section_processor(extension_name, processor)
2297        } else {
2298            Err(EditorError::ExtensionError {
2299                extension: extension_name,
2300                message: "Cannot modify shared registry integration".to_string(),
2301            })
2302        }
2303    }
2304}
2305
2306#[cfg(test)]
2307mod tests {
2308    use super::*;
2309    #[cfg(not(feature = "std"))]
2310    use alloc::string::ToString;
2311    #[cfg(not(feature = "std"))]
2312    use alloc::vec;
2313
2314    #[test]
2315    fn document_creation() {
2316        let doc = EditorDocument::new();
2317        assert!(doc.is_empty());
2318        assert_eq!(doc.len_lines(), 1);
2319        assert!(!doc.is_modified());
2320    }
2321
2322    #[test]
2323    fn document_from_content() {
2324        let content = "[Script Info]\nTitle: Test";
2325        let doc = EditorDocument::from_content(content).unwrap();
2326        assert_eq!(doc.text(), content);
2327        assert_eq!(doc.len_bytes(), content.len());
2328    }
2329
2330    #[test]
2331    fn document_modification() {
2332        let mut doc = EditorDocument::new();
2333        doc.insert(Position::new(0), "Hello").unwrap();
2334        assert!(doc.is_modified());
2335        assert_eq!(doc.text(), "Hello");
2336    }
2337
2338    #[test]
2339    fn position_conversion() {
2340        let content = "Line 1\nLine 2\nLine 3";
2341        let doc = EditorDocument::from_content(content).unwrap();
2342
2343        // Start of second line
2344        let pos = Position::new(7); // After "Line 1\n"
2345        let lc = doc.position_to_line_column(pos).unwrap();
2346        assert_eq!(lc.line, 2);
2347        assert_eq!(lc.column, 1);
2348    }
2349
2350    #[test]
2351    fn range_operations() {
2352        let mut doc = EditorDocument::from_content("Hello World").unwrap();
2353
2354        // Delete "World"
2355        let range = Range::new(Position::new(6), Position::new(11));
2356        doc.delete(range).unwrap();
2357        assert_eq!(doc.text(), "Hello ");
2358
2359        // Replace with "Rust"
2360        doc.insert(Position::new(6), "Rust").unwrap();
2361        assert_eq!(doc.text(), "Hello Rust");
2362    }
2363
2364    #[test]
2365    fn parse_script_test() {
2366        let content = "[Script Info]\nTitle: Test\n[Events]\nDialogue: test";
2367        let doc = EditorDocument::from_content(content).unwrap();
2368
2369        // Validate should succeed
2370        doc.validate().unwrap();
2371
2372        // Parse and use script
2373        let sections_count = doc.sections_count().unwrap();
2374        assert!(sections_count > 0);
2375    }
2376
2377    #[test]
2378    fn test_edit_event_by_index() {
2379        let content = r#"[Script Info]
2380Title: Test
2381
2382[Events]
2383Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
2384Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,First event
2385Dialogue: 0,0:00:05.00,0:00:10.00,Default,,0,0,0,,Second event
2386Dialogue: 0,0:00:10.00,0:00:15.00,Default,,0,0,0,,Third event"#;
2387
2388        let mut doc = EditorDocument::from_content(content).unwrap();
2389
2390        // Edit the second event (index 1)
2391        let result = doc.edit_event_by_index(1, |_event| {
2392            vec![
2393                ("text", "Modified second event".to_string()),
2394                ("style", "NewStyle".to_string()),
2395                ("start", "0:00:06.00".to_string()),
2396            ]
2397        });
2398
2399        assert!(result.is_ok());
2400        let new_line = result.unwrap();
2401        assert!(new_line.contains("Modified second event"));
2402        assert!(new_line.contains("NewStyle"));
2403        assert!(new_line.contains("0:00:06.00"));
2404
2405        // Verify the document was updated
2406        let updated_content = doc.text();
2407        assert!(updated_content.contains("Modified second event"));
2408        assert!(updated_content.contains("NewStyle"));
2409        assert!(updated_content.contains("0:00:06.00"));
2410        assert!(!updated_content.contains("Second event")); // Old text should be gone
2411    }
2412
2413    #[test]
2414    fn test_edit_event_by_index_out_of_bounds() {
2415        let content = r#"[Script Info]
2416Title: Test
2417
2418[Events]
2419Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
2420Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,First event"#;
2421
2422        let mut doc = EditorDocument::from_content(content).unwrap();
2423
2424        // Try to edit non-existent event
2425        let result =
2426            doc.edit_event_by_index(5, |_event| vec![("text", "This should fail".to_string())]);
2427
2428        assert!(result.is_err());
2429        match result.err().unwrap() {
2430            EditorError::InvalidRange { start, end, length } => {
2431                assert_eq!(start, 5);
2432                assert_eq!(end, 6);
2433                assert_eq!(length, 1); // Only 1 event exists
2434            }
2435            _ => panic!("Expected InvalidRange error"),
2436        }
2437    }
2438
2439    #[test]
2440    fn test_edit_event_with_builder() {
2441        let content = r#"[Script Info]
2442Title: Test
2443
2444[Events]
2445Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
2446Dialogue: 0,0:00:00.00,0:00:05.00,Default,Speaker,0,0,0,,Original text
2447Dialogue: 0,0:00:05.00,0:00:10.00,Default,,0,0,0,,Second event"#;
2448
2449        let mut doc = EditorDocument::from_content(content).unwrap();
2450
2451        // Edit the first event using builder
2452        let result = doc.edit_event_with_builder(0, |builder| {
2453            builder
2454                .text("Modified with builder")
2455                .style("NewStyle")
2456                .end_time("0:00:08.00")
2457                .speaker("NewSpeaker")
2458        });
2459
2460        assert!(result.is_ok());
2461        let new_line = result.unwrap();
2462        assert!(new_line.contains("Modified with builder"));
2463        assert!(new_line.contains("NewStyle"));
2464        assert!(new_line.contains("0:00:08.00"));
2465        assert!(new_line.contains("NewSpeaker"));
2466
2467        // Verify the document was updated
2468        let updated_content = doc.text();
2469        assert!(updated_content.contains("Modified with builder"));
2470        assert!(updated_content.contains("0:00:08.00"));
2471        assert!(!updated_content.contains("Original text"));
2472    }
2473
2474    #[test]
2475    fn test_edit_event_with_builder_preserves_format() {
2476        // Test with V4++ format that includes MarginT and MarginB
2477        let content = r#"[Script Info]
2478Title: Test
2479
2480[Events]
2481Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginT, MarginB, Effect, Text
2482Dialogue: 0,0:00:00.00,0:00:05.00,Default,,10,20,5,15,fade,Original text"#;
2483
2484        let mut doc = EditorDocument::from_content(content).unwrap();
2485
2486        // Edit preserving the V4++ format
2487        let result = doc.edit_event_with_builder(0, |builder| {
2488            builder.text("New text").margin_top(30).margin_bottom(40)
2489        });
2490
2491        assert!(result.is_ok());
2492        let new_line = result.unwrap();
2493
2494        // Should use MarginT and MarginB fields based on format line
2495        assert!(new_line.contains("30")); // margin_top
2496        assert!(new_line.contains("40")); // margin_bottom
2497        assert!(new_line.contains("New text"));
2498        assert!(new_line.contains("10,20,30,40")); // All margins in correct order
2499    }
2500
2501    #[test]
2502    fn test_edit_event_with_builder_comment() {
2503        let content = r#"[Script Info]
2504Title: Test
2505
2506[Events]
2507Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
2508Comment: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,This is a comment"#;
2509
2510        let mut doc = EditorDocument::from_content(content).unwrap();
2511
2512        // Edit comment event
2513        let result = doc.edit_event_with_builder(0, |builder| builder.text("Updated comment"));
2514
2515        assert!(result.is_ok());
2516        let new_line = result.unwrap();
2517        assert!(new_line.starts_with("Comment:"));
2518        assert!(new_line.contains("Updated comment"));
2519    }
2520
2521    #[test]
2522    fn test_edit_event_by_index_all_fields() {
2523        let content = r#"[Script Info]
2524Title: Test
2525
2526[Events]
2527Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
2528Dialogue: 0,0:00:00.00,0:00:05.00,Default,Speaker,10,20,30,fade,Original text"#;
2529
2530        let mut doc = EditorDocument::from_content(content).unwrap();
2531
2532        // Edit all possible fields
2533        let result = doc.edit_event_by_index(0, |_event| {
2534            vec![
2535                ("layer", "1".to_string()),
2536                ("start", "0:00:01.00".to_string()),
2537                ("end", "0:00:06.00".to_string()),
2538                ("style", "Custom".to_string()),
2539                ("name", "NewSpeaker".to_string()),
2540                ("margin_l", "15".to_string()),
2541                ("margin_r", "25".to_string()),
2542                ("margin_v", "35".to_string()),
2543                ("effect", "scroll".to_string()),
2544                ("text", "Completely new text".to_string()),
2545            ]
2546        });
2547
2548        assert!(result.is_ok());
2549        let new_line = result.unwrap();
2550
2551        // Verify all fields were updated
2552        assert!(new_line.contains("Dialogue: 1,"));
2553        assert!(new_line.contains("0:00:01.00"));
2554        assert!(new_line.contains("0:00:06.00"));
2555        assert!(new_line.contains("Custom"));
2556        assert!(new_line.contains("NewSpeaker"));
2557        assert!(new_line.contains("15"));
2558        assert!(new_line.contains("25"));
2559        assert!(new_line.contains("35"));
2560        assert!(new_line.contains("scroll"));
2561        assert!(new_line.contains("Completely new text"));
2562    }
2563
2564    #[test]
2565    fn test_undo_redo_basic() {
2566        let mut doc = EditorDocument::from_content("[Script Info]\nTitle: Test").unwrap();
2567        let initial_len = doc.len_bytes();
2568        // println!(
2569        //     "Initial doc length: {}, content: {:?}",
2570        //     initial_len,
2571        //     doc.text()
2572        // );
2573
2574        // Insert some text
2575        doc.insert(Position::new(initial_len), "\nAuthor: John")
2576            .unwrap();
2577        // println!(
2578        //     "After insert: length: {}, content: {:?}",
2579        //     doc.len_bytes(),
2580        //     doc.text()
2581        // );
2582        assert!(doc.text().contains("Author: John"));
2583        assert!(doc.can_undo());
2584        assert!(!doc.can_redo());
2585
2586        // Undo the insert
2587        let result = doc.undo().unwrap();
2588        // println!(
2589        //     "After undo: length: {}, content: {:?}",
2590        //     doc.len_bytes(),
2591        //     doc.text()
2592        // );
2593        assert!(result.success);
2594        assert!(!doc.text().contains("Author: John"));
2595        assert!(!doc.can_undo());
2596        assert!(doc.can_redo());
2597
2598        // Redo the insert
2599        // println!("About to redo...");
2600        let result = doc.redo().unwrap();
2601        // println!(
2602        //     "After redo: length: {}, content: {:?}",
2603        //     doc.len_bytes(),
2604        //     doc.text()
2605        // );
2606        assert!(result.success);
2607        assert!(doc.text().contains("Author: John"));
2608        assert!(doc.can_undo());
2609        assert!(!doc.can_redo());
2610    }
2611
2612    #[test]
2613    fn test_undo_redo_multiple_operations() {
2614        let mut doc = EditorDocument::from_content("[Script Info]\nTitle: Test").unwrap();
2615
2616        // Multiple operations
2617        doc.insert(Position::new(doc.len_bytes()), "\nAuthor: John")
2618            .unwrap();
2619        doc.insert(Position::new(doc.len_bytes()), "\nVersion: 1.0")
2620            .unwrap();
2621        doc.insert(Position::new(doc.len_bytes()), "\nComment: Test script")
2622            .unwrap();
2623
2624        assert!(doc.text().contains("Author: John"));
2625        assert!(doc.text().contains("Version: 1.0"));
2626        assert!(doc.text().contains("Comment: Test script"));
2627
2628        // Undo all operations
2629        doc.undo().unwrap();
2630        assert!(!doc.text().contains("Comment: Test script"));
2631
2632        doc.undo().unwrap();
2633        assert!(!doc.text().contains("Version: 1.0"));
2634
2635        doc.undo().unwrap();
2636        assert!(!doc.text().contains("Author: John"));
2637
2638        // Redo one operation
2639        doc.redo().unwrap();
2640        assert!(doc.text().contains("Author: John"));
2641        assert!(!doc.text().contains("Version: 1.0"));
2642    }
2643
2644    #[test]
2645    fn test_undo_redo_replace() {
2646        let mut doc = EditorDocument::from_content("[Script Info]\nTitle: Original").unwrap();
2647
2648        // Find and replace "Original" with "Modified"
2649        let start = doc.text().find("Original").unwrap();
2650        let range = Range::new(Position::new(start), Position::new(start + 8));
2651        doc.replace(range, "Modified").unwrap();
2652
2653        assert!(doc.text().contains("Title: Modified"));
2654        assert!(!doc.text().contains("Original"));
2655
2656        // Undo the replace
2657        doc.undo().unwrap();
2658        assert!(doc.text().contains("Title: Original"));
2659        assert!(!doc.text().contains("Modified"));
2660
2661        // Redo the replace
2662        doc.redo().unwrap();
2663        assert!(doc.text().contains("Title: Modified"));
2664        assert!(!doc.text().contains("Original"));
2665    }
2666
2667    #[test]
2668    fn test_validator_integration() {
2669        let mut doc = EditorDocument::from_content(
2670            "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Test"
2671        ).unwrap();
2672
2673        // Should have result after comprehensive validation
2674        let result = doc.validate_comprehensive().unwrap();
2675        assert!(result.is_valid);
2676
2677        // Modify document
2678        doc.insert(Position::new(doc.len_bytes()), "\nComment: Test")
2679            .unwrap();
2680
2681        // Force validate should work
2682        let result2 = doc.force_validate().unwrap();
2683        assert!(result2.is_valid);
2684    }
2685
2686    #[test]
2687    fn test_validator_configuration() {
2688        let mut doc = EditorDocument::new();
2689
2690        // Configure validator
2691        let config = crate::utils::validator::ValidatorConfig {
2692            max_issues: 5,
2693            enable_performance_hints: false,
2694            ..Default::default()
2695        };
2696        doc.set_validator_config(config);
2697
2698        // Validator should be configured
2699        // We can't directly check the cache anymore, but configuration should work
2700        assert!(doc.is_valid_cached().is_ok());
2701    }
2702
2703    #[test]
2704    fn test_validator_with_invalid_document() {
2705        let mut doc = EditorDocument::from_content("Invalid content").unwrap();
2706
2707        // Comprehensive validation should find issues
2708        let result = doc.validate_comprehensive().unwrap();
2709        assert!(!result.issues.is_empty());
2710
2711        // Should have warnings about missing sections
2712        let warnings =
2713            result.issues_with_severity(crate::utils::validator::ValidationSeverity::Warning);
2714        assert!(!warnings.is_empty());
2715    }
2716
2717    #[test]
2718    #[cfg(feature = "formats")]
2719    fn test_format_import_export() {
2720        // Test SRT import
2721        let srt_content = "1\n00:00:00,000 --> 00:00:05,000\nHello world!";
2722        let doc = EditorDocument::import_format(
2723            srt_content,
2724            Some(crate::utils::formats::SubtitleFormat::SRT),
2725        )
2726        .unwrap();
2727        assert!(doc.text().contains("Hello world!"));
2728        assert!(doc.has_events().unwrap());
2729
2730        // Test export to WebVTT
2731        let options = crate::utils::formats::ConversionOptions::default();
2732        let webvtt = doc
2733            .export_format(crate::utils::formats::SubtitleFormat::WebVTT, &options)
2734            .unwrap();
2735        assert!(webvtt.starts_with("WEBVTT"));
2736        assert!(webvtt.contains("00:00:00.000 --> 00:00:05.000"));
2737        assert!(webvtt.contains("Hello world!"));
2738    }
2739
2740    #[test]
2741    fn test_undo_redo_delete() {
2742        let mut doc =
2743            EditorDocument::from_content("[Script Info]\nTitle: Test\nAuthor: John").unwrap();
2744
2745        // Delete the Author line
2746        let start = doc.text().find("\nAuthor: John").unwrap();
2747        let range = Range::new(Position::new(start), Position::new(start + 13));
2748        doc.delete(range).unwrap();
2749
2750        assert!(!doc.text().contains("Author: John"));
2751
2752        // Undo the delete
2753        doc.undo().unwrap();
2754        assert!(doc.text().contains("Author: John"));
2755
2756        // Redo the delete
2757        doc.redo().unwrap();
2758        assert!(!doc.text().contains("Author: John"));
2759    }
2760
2761    #[cfg(feature = "plugins")]
2762    #[test]
2763    fn test_registry_integration() {
2764        let mut doc = EditorDocument::new();
2765
2766        // Initially no registry
2767        assert!(doc.registry().is_none());
2768
2769        // Initialize with registry
2770        doc.initialize_registry().unwrap();
2771        assert!(doc.registry().is_some());
2772
2773        // Parse with extensions
2774        doc.insert(Position::new(0), "[Script Info]\nTitle: Test\n\n[Events]\nDialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,{\\b1}Bold{\\b0} text").unwrap();
2775
2776        let section_count = doc
2777            .parse_with_extensions(|script| script.sections().len())
2778            .unwrap();
2779        assert_eq!(section_count, 2);
2780    }
2781
2782    #[cfg(feature = "plugins")]
2783    #[test]
2784    fn test_custom_tag_handler() {
2785        use ass_core::plugin::{TagHandler, TagResult};
2786
2787        struct CustomHandler;
2788        impl TagHandler for CustomHandler {
2789            fn name(&self) -> &'static str {
2790                "custom"
2791            }
2792
2793            fn process(&self, _args: &str) -> TagResult {
2794                TagResult::Processed
2795            }
2796
2797            fn validate(&self, _args: &str) -> bool {
2798                true
2799            }
2800        }
2801
2802        let mut doc = EditorDocument::new();
2803        doc.initialize_registry().unwrap();
2804
2805        // Register custom tag handler
2806        assert!(doc
2807            .register_tag_handler("test-extension".to_string(), Box::new(CustomHandler))
2808            .is_ok());
2809    }
2810
2811    #[cfg(feature = "plugins")]
2812    #[test]
2813    fn test_custom_section_processor() {
2814        use ass_core::plugin::{SectionProcessor, SectionResult};
2815
2816        struct CustomProcessor;
2817        impl SectionProcessor for CustomProcessor {
2818            fn name(&self) -> &'static str {
2819                "CustomSection"
2820            }
2821
2822            fn process(&self, _header: &str, _lines: &[&str]) -> SectionResult {
2823                SectionResult::Processed
2824            }
2825
2826            fn validate(&self, _header: &str, _lines: &[&str]) -> bool {
2827                true
2828            }
2829        }
2830
2831        let mut doc = EditorDocument::new();
2832        doc.initialize_registry().unwrap();
2833
2834        // Register custom section processor
2835        assert!(doc
2836            .register_section_processor("test-extension".to_string(), Box::new(CustomProcessor))
2837            .is_ok());
2838    }
2839}