ass_editor/commands/
mod.rs

1//! Command system for editor operations
2//!
3//! Provides a trait-based command system with fluent APIs for text editing,
4//! undo/redo support, and extensible command types. All commands are atomic
5//! and can be undone/redone efficiently.
6
7pub mod delta_commands;
8pub mod event_commands;
9pub mod karaoke_commands;
10pub mod macros;
11pub mod style_commands;
12pub mod tag_commands;
13
14use crate::core::{EditorDocument, EditorError, Position, Range, Result};
15
16#[cfg(feature = "stream")]
17use ass_core::parser::ScriptDeltaOwned;
18
19// Re-export delta commands, event commands, karaoke commands, style commands, and tag commands
20pub use delta_commands::*;
21pub use event_commands::*;
22pub use karaoke_commands::*;
23pub use style_commands::*;
24pub use tag_commands::*;
25
26#[cfg(not(feature = "std"))]
27use alloc::{
28    boxed::Box,
29    format,
30    string::{String, ToString},
31    vec::Vec,
32};
33
34/// Result of executing a command
35///
36/// Contains the modified document and optional metadata about the operation.
37/// This will be used by the history system to track changes.
38#[derive(Debug, Clone)]
39pub struct CommandResult {
40    /// Whether the command was successfully executed
41    pub success: bool,
42
43    /// Optional message about the operation
44    pub message: Option<String>,
45
46    /// The range of text that was modified (for cursor updates)
47    pub modified_range: Option<Range>,
48
49    /// New cursor position after the command
50    pub new_cursor: Option<Position>,
51
52    /// Whether the document content was changed
53    pub content_changed: bool,
54
55    /// Script delta for incremental parsing (when available)
56    #[cfg(feature = "stream")]
57    pub script_delta: Option<ScriptDeltaOwned>,
58}
59
60impl CommandResult {
61    /// Create a successful command result
62    pub fn success() -> Self {
63        Self {
64            success: true,
65            message: None,
66            modified_range: None,
67            new_cursor: None,
68            content_changed: false,
69            #[cfg(feature = "stream")]
70            script_delta: None,
71        }
72    }
73
74    /// Create a successful result with content change
75    pub fn success_with_change(range: Range, cursor: Position) -> Self {
76        Self {
77            success: true,
78            message: None,
79            modified_range: Some(range),
80            new_cursor: Some(cursor),
81            content_changed: true,
82            #[cfg(feature = "stream")]
83            script_delta: None,
84        }
85    }
86
87    /// Create a failed command result
88    pub fn failure(message: String) -> Self {
89        Self {
90            success: false,
91            message: Some(message),
92            modified_range: None,
93            new_cursor: None,
94            content_changed: false,
95            #[cfg(feature = "stream")]
96            script_delta: None,
97        }
98    }
99
100    /// Add a script delta to the result
101    #[cfg(feature = "stream")]
102    #[must_use]
103    pub fn with_delta(mut self, delta: ScriptDeltaOwned) -> Self {
104        self.script_delta = Some(delta);
105        self
106    }
107
108    /// Add a message to the result
109    #[must_use]
110    pub fn with_message(mut self, message: String) -> Self {
111        self.message = Some(message);
112        self
113    }
114}
115
116/// Trait for editor commands that can be executed and undone
117///
118/// All commands implement this trait to provide a consistent interface
119/// for execution, undo/redo, and introspection.
120///
121/// # Examples
122///
123/// Creating a custom command:
124///
125/// ```
126/// use ass_editor::{EditorCommand, EditorDocument, CommandResult, Result, Position, Range};
127///
128/// #[derive(Debug)]
129/// struct UppercaseCommand {
130///     description: String,
131/// }
132///
133/// impl UppercaseCommand {
134///     fn new() -> Self {
135///         Self {
136///             description: "Convert to uppercase".to_string(),
137///         }
138///     }
139/// }
140///
141/// impl EditorCommand for UppercaseCommand {
142///     fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
143///         let text = document.text().to_uppercase();
144///         let range = Range::new(Position::new(0), Position::new(document.len()));
145///         document.replace(range, &text)?;
146///         Ok(CommandResult::success().with_message("Text converted to uppercase".to_string()))
147///     }
148///
149///     fn description(&self) -> &str {
150///         &self.description
151///     }
152/// }
153/// ```
154pub trait EditorCommand: core::fmt::Debug + Send + Sync {
155    /// Execute the command on the given document
156    ///
157    /// Returns a result indicating success/failure and metadata about
158    /// the operation for undo/redo tracking.
159    fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult>;
160
161    /// Get a human-readable description of the command
162    fn description(&self) -> &str;
163
164    /// Check if this command modifies document content
165    ///
166    /// Used to determine if the document should be marked as modified
167    /// and whether to save undo state.
168    fn modifies_content(&self) -> bool {
169        true
170    }
171
172    /// Get the estimated memory usage of this command
173    ///
174    /// Used for memory management in undo stacks with limited capacity.
175    /// Default implementation provides a conservative estimate.
176    fn memory_usage(&self) -> usize {
177        64 // Conservative default estimate for command overhead
178    }
179}
180
181/// Text insertion command
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct InsertTextCommand {
184    /// Position to insert text at
185    pub position: Position,
186    /// Text to insert
187    pub text: String,
188    /// Optional description override
189    pub description: Option<String>,
190}
191
192impl InsertTextCommand {
193    /// Create a new insert text command
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// use ass_editor::{InsertTextCommand, EditorDocument, Position, EditorCommand};
199    ///
200    /// let mut doc = EditorDocument::new();
201    /// let command = InsertTextCommand::new(Position::new(0), "Hello World".to_string());
202    ///
203    /// let result = command.execute(&mut doc).unwrap();
204    /// assert!(result.success);
205    /// assert_eq!(doc.text(), "Hello World");
206    /// ```
207    pub fn new(position: Position, text: String) -> Self {
208        Self {
209            position,
210            text,
211            description: None,
212        }
213    }
214
215    /// Set a custom description for this command
216    #[must_use]
217    pub fn with_description(mut self, description: String) -> Self {
218        self.description = Some(description);
219        self
220    }
221}
222
223impl EditorCommand for InsertTextCommand {
224    fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
225        document.insert_raw(self.position, &self.text)?;
226
227        let end_pos = Position::new(self.position.offset + self.text.len());
228        let range = Range::new(self.position, end_pos);
229
230        Ok(CommandResult::success_with_change(range, end_pos))
231    }
232
233    fn description(&self) -> &str {
234        self.description.as_deref().unwrap_or("Insert text")
235    }
236
237    fn memory_usage(&self) -> usize {
238        core::mem::size_of::<Self>()
239            + self.text.len()
240            + self.description.as_ref().map_or(0, |d| d.len())
241    }
242}
243
244/// Text deletion command
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct DeleteTextCommand {
247    /// Range of text to delete
248    pub range: Range,
249    /// Optional description override
250    pub description: Option<String>,
251}
252
253impl DeleteTextCommand {
254    /// Create a new delete text command
255    pub fn new(range: Range) -> Self {
256        Self {
257            range,
258            description: None,
259        }
260    }
261
262    /// Set a custom description for this command
263    #[must_use]
264    pub fn with_description(mut self, description: String) -> Self {
265        self.description = Some(description);
266        self
267    }
268}
269
270impl EditorCommand for DeleteTextCommand {
271    fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
272        document.delete_raw(self.range)?;
273
274        let cursor_pos = self.range.start;
275        let range = Range::new(self.range.start, self.range.start);
276
277        Ok(CommandResult::success_with_change(range, cursor_pos))
278    }
279
280    fn description(&self) -> &str {
281        self.description.as_deref().unwrap_or("Delete text")
282    }
283}
284
285/// Text replacement command
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct ReplaceTextCommand {
288    /// Range of text to replace
289    pub range: Range,
290    /// New text to insert
291    pub new_text: String,
292    /// Optional description override
293    pub description: Option<String>,
294}
295
296impl ReplaceTextCommand {
297    /// Create a new replace text command
298    pub fn new(range: Range, new_text: String) -> Self {
299        Self {
300            range,
301            new_text,
302            description: None,
303        }
304    }
305
306    /// Set a custom description for this command
307    #[must_use]
308    pub fn with_description(mut self, description: String) -> Self {
309        self.description = Some(description);
310        self
311    }
312}
313
314impl EditorCommand for ReplaceTextCommand {
315    fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
316        document.replace_raw(self.range, &self.new_text)?;
317
318        let end_pos = Position::new(self.range.start.offset + self.new_text.len());
319        let range = Range::new(self.range.start, end_pos);
320
321        Ok(CommandResult::success_with_change(range, end_pos))
322    }
323
324    fn description(&self) -> &str {
325        self.description.as_deref().unwrap_or("Replace text")
326    }
327
328    fn memory_usage(&self) -> usize {
329        core::mem::size_of::<Self>()
330            + self.new_text.len()
331            + self.description.as_ref().map_or(0, |d| d.len())
332    }
333}
334
335/// Batch command that executes multiple commands as a single atomic operation
336#[derive(Debug)]
337pub struct BatchCommand {
338    /// Commands to execute in order
339    pub commands: Vec<Box<dyn EditorCommand>>,
340    /// Description of the batch operation
341    pub description: String,
342}
343
344impl BatchCommand {
345    /// Create a new batch command
346    ///
347    /// # Examples
348    ///
349    /// ```
350    /// use ass_editor::{BatchCommand, InsertTextCommand, DeleteTextCommand, Position, Range, EditorDocument, EditorCommand};
351    ///
352    /// let mut doc = EditorDocument::from_content("Hello World").unwrap();
353    ///
354    /// let batch = BatchCommand::new("Multiple operations".to_string())
355    ///     .add_command(Box::new(InsertTextCommand::new(Position::new(5), " beautiful".to_string())))
356    ///     .add_command(Box::new(DeleteTextCommand::new(Range::new(Position::new(15), Position::new(21)))));
357    ///
358    /// let result = batch.execute(&mut doc).unwrap();
359    /// assert!(result.success);
360    /// assert_eq!(doc.text(), "Hello beautiful");
361    /// ```
362    pub fn new(description: String) -> Self {
363        Self {
364            commands: Vec::new(),
365            description,
366        }
367    }
368
369    /// Add a command to the batch
370    pub fn add_command(mut self, command: Box<dyn EditorCommand>) -> Self {
371        self.commands.push(command);
372        self
373    }
374
375    /// Add multiple commands to the batch
376    pub fn add_commands(mut self, commands: Vec<Box<dyn EditorCommand>>) -> Self {
377        self.commands.extend(commands);
378        self
379    }
380}
381
382impl EditorCommand for BatchCommand {
383    fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
384        let mut overall_result = CommandResult::success();
385        let mut first_range: Option<Range> = None;
386        let mut last_cursor: Option<Position> = None;
387
388        for command in &self.commands {
389            let result = command.execute(document)?;
390
391            if !result.success {
392                return Ok(CommandResult::failure(format!(
393                    "Batch command failed at: {}",
394                    command.description()
395                )));
396            }
397
398            // Track the overall range of changes
399            if let Some(range) = result.modified_range {
400                first_range = Some(match first_range {
401                    Some(existing) => existing.union(&range),
402                    None => range,
403                });
404            }
405
406            if result.new_cursor.is_some() {
407                last_cursor = result.new_cursor;
408            }
409
410            if result.content_changed {
411                overall_result.content_changed = true;
412            }
413        }
414
415        overall_result.modified_range = first_range;
416        overall_result.new_cursor = last_cursor;
417
418        Ok(overall_result)
419    }
420
421    fn description(&self) -> &str {
422        &self.description
423    }
424
425    fn memory_usage(&self) -> usize {
426        core::mem::size_of::<Self>()
427            + self.description.len()
428            + self
429                .commands
430                .iter()
431                .map(|c| c.memory_usage())
432                .sum::<usize>()
433    }
434}
435
436/// Fluent API builder for creating and executing commands
437///
438/// Provides an ergonomic way to build and execute commands:
439/// ```
440/// use ass_editor::{EditorDocument, Position, TextCommand};
441///
442/// let mut doc = EditorDocument::from_content("Hello world!").unwrap();
443/// let position = Position::new(5);
444///
445/// let result = TextCommand::new(&mut doc)
446///     .at(position)
447///     .insert(" beautiful")
448///     .unwrap();
449///
450/// assert_eq!(doc.text(), "Hello beautiful world!");
451/// ```
452pub struct TextCommand<'a> {
453    document: &'a mut EditorDocument,
454    position: Option<Position>,
455    range: Option<Range>,
456}
457
458impl<'a> TextCommand<'a> {
459    /// Create a new text command builder
460    pub fn new(document: &'a mut EditorDocument) -> Self {
461        Self {
462            document,
463            position: None,
464            range: None,
465        }
466    }
467
468    /// Set the position for the operation
469    #[must_use]
470    pub fn at(mut self, position: Position) -> Self {
471        self.position = Some(position);
472        self
473    }
474
475    /// Set the range for the operation
476    #[must_use]
477    pub fn range(mut self, range: Range) -> Self {
478        self.range = Some(range);
479        self
480    }
481
482    /// Insert text at the current position
483    pub fn insert(self, text: &str) -> Result<CommandResult> {
484        let position = self
485            .position
486            .ok_or_else(|| EditorError::command_failed("Position not set for insert operation"))?;
487
488        let command = InsertTextCommand::new(position, text.to_string());
489        command.execute(self.document)
490    }
491
492    /// Delete text in the current range
493    pub fn delete(self) -> Result<CommandResult> {
494        let range = self
495            .range
496            .ok_or_else(|| EditorError::command_failed("Range not set for delete operation"))?;
497
498        let command = DeleteTextCommand::new(range);
499        command.execute(self.document)
500    }
501
502    /// Replace text in the current range
503    pub fn replace(self, new_text: &str) -> Result<CommandResult> {
504        let range = self
505            .range
506            .ok_or_else(|| EditorError::command_failed("Range not set for replace operation"))?;
507
508        let command = ReplaceTextCommand::new(range, new_text.to_string());
509        command.execute(self.document)
510    }
511}
512
513// Extension trait to add fluent command methods to EditorDocument
514pub trait DocumentCommandExt {
515    /// Start a fluent command chain
516    fn command(&mut self) -> TextCommand<'_>;
517
518    /// Quick insert at position
519    fn insert_at(&mut self, position: Position, text: &str) -> Result<CommandResult>;
520
521    /// Quick delete range
522    fn delete_range(&mut self, range: Range) -> Result<CommandResult>;
523
524    /// Quick replace range
525    fn replace_range(&mut self, range: Range, text: &str) -> Result<CommandResult>;
526}
527
528impl DocumentCommandExt for EditorDocument {
529    fn command(&mut self) -> TextCommand<'_> {
530        TextCommand::new(self)
531    }
532
533    fn insert_at(&mut self, position: Position, text: &str) -> Result<CommandResult> {
534        let command = InsertTextCommand::new(position, text.to_string());
535        command.execute(self)
536    }
537
538    fn delete_range(&mut self, range: Range) -> Result<CommandResult> {
539        let command = DeleteTextCommand::new(range);
540        command.execute(self)
541    }
542
543    fn replace_range(&mut self, range: Range, text: &str) -> Result<CommandResult> {
544        let command = ReplaceTextCommand::new(range, text.to_string());
545        command.execute(self)
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552    use crate::core::EditorDocument;
553    #[cfg(not(feature = "std"))]
554    use alloc::string::ToString;
555    #[cfg(not(feature = "std"))]
556    #[test]
557    fn insert_command_execution() {
558        let mut doc = EditorDocument::new();
559        let command = InsertTextCommand::new(Position::new(0), "Hello".to_string());
560
561        let result = command.execute(&mut doc).unwrap();
562        assert!(result.success);
563        assert!(result.content_changed);
564        assert_eq!(doc.text(), "Hello");
565    }
566
567    #[test]
568    fn delete_command_execution() {
569        let mut doc = EditorDocument::from_content("Hello World").unwrap();
570        let range = Range::new(Position::new(6), Position::new(11));
571        let command = DeleteTextCommand::new(range);
572
573        let result = command.execute(&mut doc).unwrap();
574        assert!(result.success);
575        assert!(result.content_changed);
576        assert_eq!(doc.text(), "Hello ");
577    }
578
579    #[test]
580    fn replace_command_execution() {
581        let mut doc = EditorDocument::from_content("Hello World").unwrap();
582        let range = Range::new(Position::new(6), Position::new(11));
583        let command = ReplaceTextCommand::new(range, "Rust".to_string());
584
585        let result = command.execute(&mut doc).unwrap();
586        assert!(result.success);
587        assert!(result.content_changed);
588        assert_eq!(doc.text(), "Hello Rust");
589    }
590
591    #[test]
592    fn batch_command_execution() {
593        let mut doc = EditorDocument::from_content("Hello").unwrap();
594
595        let batch = BatchCommand::new("Insert and replace".to_string())
596            .add_command(Box::new(InsertTextCommand::new(
597                Position::new(5),
598                " World".to_string(),
599            )))
600            .add_command(Box::new(ReplaceTextCommand::new(
601                Range::new(Position::new(0), Position::new(5)),
602                "Hi".to_string(),
603            )));
604
605        let result = batch.execute(&mut doc).unwrap();
606        assert!(result.success);
607        assert!(result.content_changed);
608        assert_eq!(doc.text(), "Hi World");
609    }
610
611    #[test]
612    fn fluent_api_usage() {
613        let mut doc = EditorDocument::new();
614
615        // Test fluent insertion
616        let result = doc.command().at(Position::new(0)).insert("Hello").unwrap();
617
618        assert!(result.success);
619        assert_eq!(doc.text(), "Hello");
620
621        // Test fluent replacement
622        let range = Range::new(Position::new(0), Position::new(5));
623        let result = doc.command().range(range).replace("Hi").unwrap();
624
625        assert!(result.success);
626        assert_eq!(doc.text(), "Hi");
627    }
628
629    #[test]
630    fn document_extension_methods() {
631        let mut doc = EditorDocument::new();
632
633        // Test insert_at
634        doc.insert_at(Position::new(0), "Hello").unwrap();
635        assert_eq!(doc.text(), "Hello");
636
637        // Test replace_range
638        let range = Range::new(Position::new(0), Position::new(5));
639        doc.replace_range(range, "Hi").unwrap();
640        assert_eq!(doc.text(), "Hi");
641
642        // Test delete_range
643        let range = Range::new(Position::new(0), Position::new(2));
644        doc.delete_range(range).unwrap();
645        assert_eq!(doc.text(), "");
646    }
647
648    #[test]
649    fn command_memory_usage() {
650        let insert_cmd = InsertTextCommand::new(Position::new(0), "Hello".to_string());
651        let usage = insert_cmd.memory_usage();
652
653        // Should account for the struct size plus string length
654        assert!(usage >= core::mem::size_of::<InsertTextCommand>() + 5);
655    }
656}