pub mod delta_commands;
pub mod event_commands;
pub mod karaoke_commands;
pub mod macros;
pub mod style_commands;
pub mod tag_commands;
use crate::core::{EditorDocument, EditorError, Position, Range, Result};
#[cfg(feature = "stream")]
use ass_core::parser::ScriptDeltaOwned;
pub use delta_commands::*;
pub use event_commands::*;
pub use karaoke_commands::*;
pub use style_commands::*;
pub use tag_commands::*;
#[cfg(not(feature = "std"))]
use alloc::{
boxed::Box,
format,
string::{String, ToString},
vec::Vec,
};
#[derive(Debug, Clone)]
pub struct CommandResult {
pub success: bool,
pub message: Option<String>,
pub modified_range: Option<Range>,
pub new_cursor: Option<Position>,
pub content_changed: bool,
#[cfg(feature = "stream")]
pub script_delta: Option<ScriptDeltaOwned>,
}
impl CommandResult {
pub fn success() -> Self {
Self {
success: true,
message: None,
modified_range: None,
new_cursor: None,
content_changed: false,
#[cfg(feature = "stream")]
script_delta: None,
}
}
pub fn success_with_change(range: Range, cursor: Position) -> Self {
Self {
success: true,
message: None,
modified_range: Some(range),
new_cursor: Some(cursor),
content_changed: true,
#[cfg(feature = "stream")]
script_delta: None,
}
}
pub fn failure(message: String) -> Self {
Self {
success: false,
message: Some(message),
modified_range: None,
new_cursor: None,
content_changed: false,
#[cfg(feature = "stream")]
script_delta: None,
}
}
#[cfg(feature = "stream")]
#[must_use]
pub fn with_delta(mut self, delta: ScriptDeltaOwned) -> Self {
self.script_delta = Some(delta);
self
}
#[must_use]
pub fn with_message(mut self, message: String) -> Self {
self.message = Some(message);
self
}
}
pub trait EditorCommand: core::fmt::Debug + Send + Sync {
fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult>;
fn description(&self) -> &str;
fn modifies_content(&self) -> bool {
true
}
fn memory_usage(&self) -> usize {
64 }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InsertTextCommand {
pub position: Position,
pub text: String,
pub description: Option<String>,
}
impl InsertTextCommand {
pub fn new(position: Position, text: String) -> Self {
Self {
position,
text,
description: None,
}
}
#[must_use]
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
}
impl EditorCommand for InsertTextCommand {
fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
document.insert_raw(self.position, &self.text)?;
let end_pos = Position::new(self.position.offset + self.text.len());
let range = Range::new(self.position, end_pos);
Ok(CommandResult::success_with_change(range, end_pos))
}
fn description(&self) -> &str {
self.description.as_deref().unwrap_or("Insert text")
}
fn memory_usage(&self) -> usize {
core::mem::size_of::<Self>()
+ self.text.len()
+ self.description.as_ref().map_or(0, |d| d.len())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteTextCommand {
pub range: Range,
pub description: Option<String>,
}
impl DeleteTextCommand {
pub fn new(range: Range) -> Self {
Self {
range,
description: None,
}
}
#[must_use]
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
}
impl EditorCommand for DeleteTextCommand {
fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
document.delete_raw(self.range)?;
let cursor_pos = self.range.start;
let range = Range::new(self.range.start, self.range.start);
Ok(CommandResult::success_with_change(range, cursor_pos))
}
fn description(&self) -> &str {
self.description.as_deref().unwrap_or("Delete text")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReplaceTextCommand {
pub range: Range,
pub new_text: String,
pub description: Option<String>,
}
impl ReplaceTextCommand {
pub fn new(range: Range, new_text: String) -> Self {
Self {
range,
new_text,
description: None,
}
}
#[must_use]
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
}
impl EditorCommand for ReplaceTextCommand {
fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
document.replace_raw(self.range, &self.new_text)?;
let end_pos = Position::new(self.range.start.offset + self.new_text.len());
let range = Range::new(self.range.start, end_pos);
Ok(CommandResult::success_with_change(range, end_pos))
}
fn description(&self) -> &str {
self.description.as_deref().unwrap_or("Replace text")
}
fn memory_usage(&self) -> usize {
core::mem::size_of::<Self>()
+ self.new_text.len()
+ self.description.as_ref().map_or(0, |d| d.len())
}
}
#[derive(Debug)]
pub struct BatchCommand {
pub commands: Vec<Box<dyn EditorCommand>>,
pub description: String,
}
impl BatchCommand {
pub fn new(description: String) -> Self {
Self {
commands: Vec::new(),
description,
}
}
pub fn add_command(mut self, command: Box<dyn EditorCommand>) -> Self {
self.commands.push(command);
self
}
pub fn add_commands(mut self, commands: Vec<Box<dyn EditorCommand>>) -> Self {
self.commands.extend(commands);
self
}
}
impl EditorCommand for BatchCommand {
fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
let mut overall_result = CommandResult::success();
let mut first_range: Option<Range> = None;
let mut last_cursor: Option<Position> = None;
for command in &self.commands {
let result = command.execute(document)?;
if !result.success {
return Ok(CommandResult::failure(format!(
"Batch command failed at: {}",
command.description()
)));
}
if let Some(range) = result.modified_range {
first_range = Some(match first_range {
Some(existing) => existing.union(&range),
None => range,
});
}
if result.new_cursor.is_some() {
last_cursor = result.new_cursor;
}
if result.content_changed {
overall_result.content_changed = true;
}
}
overall_result.modified_range = first_range;
overall_result.new_cursor = last_cursor;
Ok(overall_result)
}
fn description(&self) -> &str {
&self.description
}
fn memory_usage(&self) -> usize {
core::mem::size_of::<Self>()
+ self.description.len()
+ self
.commands
.iter()
.map(|c| c.memory_usage())
.sum::<usize>()
}
}
pub struct TextCommand<'a> {
document: &'a mut EditorDocument,
position: Option<Position>,
range: Option<Range>,
}
impl<'a> TextCommand<'a> {
pub fn new(document: &'a mut EditorDocument) -> Self {
Self {
document,
position: None,
range: None,
}
}
#[must_use]
pub fn at(mut self, position: Position) -> Self {
self.position = Some(position);
self
}
#[must_use]
pub fn range(mut self, range: Range) -> Self {
self.range = Some(range);
self
}
pub fn insert(self, text: &str) -> Result<CommandResult> {
let position = self
.position
.ok_or_else(|| EditorError::command_failed("Position not set for insert operation"))?;
let command = InsertTextCommand::new(position, text.to_string());
command.execute(self.document)
}
pub fn delete(self) -> Result<CommandResult> {
let range = self
.range
.ok_or_else(|| EditorError::command_failed("Range not set for delete operation"))?;
let command = DeleteTextCommand::new(range);
command.execute(self.document)
}
pub fn replace(self, new_text: &str) -> Result<CommandResult> {
let range = self
.range
.ok_or_else(|| EditorError::command_failed("Range not set for replace operation"))?;
let command = ReplaceTextCommand::new(range, new_text.to_string());
command.execute(self.document)
}
}
pub trait DocumentCommandExt {
fn command(&mut self) -> TextCommand<'_>;
fn insert_at(&mut self, position: Position, text: &str) -> Result<CommandResult>;
fn delete_range(&mut self, range: Range) -> Result<CommandResult>;
fn replace_range(&mut self, range: Range, text: &str) -> Result<CommandResult>;
}
impl DocumentCommandExt for EditorDocument {
fn command(&mut self) -> TextCommand<'_> {
TextCommand::new(self)
}
fn insert_at(&mut self, position: Position, text: &str) -> Result<CommandResult> {
let command = InsertTextCommand::new(position, text.to_string());
command.execute(self)
}
fn delete_range(&mut self, range: Range) -> Result<CommandResult> {
let command = DeleteTextCommand::new(range);
command.execute(self)
}
fn replace_range(&mut self, range: Range, text: &str) -> Result<CommandResult> {
let command = ReplaceTextCommand::new(range, text.to_string());
command.execute(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::EditorDocument;
#[cfg(not(feature = "std"))]
use alloc::string::ToString;
#[cfg(not(feature = "std"))]
#[test]
fn insert_command_execution() {
let mut doc = EditorDocument::new();
let command = InsertTextCommand::new(Position::new(0), "Hello".to_string());
let result = command.execute(&mut doc).unwrap();
assert!(result.success);
assert!(result.content_changed);
assert_eq!(doc.text(), "Hello");
}
#[test]
fn delete_command_execution() {
let mut doc = EditorDocument::from_content("Hello World").unwrap();
let range = Range::new(Position::new(6), Position::new(11));
let command = DeleteTextCommand::new(range);
let result = command.execute(&mut doc).unwrap();
assert!(result.success);
assert!(result.content_changed);
assert_eq!(doc.text(), "Hello ");
}
#[test]
fn replace_command_execution() {
let mut doc = EditorDocument::from_content("Hello World").unwrap();
let range = Range::new(Position::new(6), Position::new(11));
let command = ReplaceTextCommand::new(range, "Rust".to_string());
let result = command.execute(&mut doc).unwrap();
assert!(result.success);
assert!(result.content_changed);
assert_eq!(doc.text(), "Hello Rust");
}
#[test]
fn batch_command_execution() {
let mut doc = EditorDocument::from_content("Hello").unwrap();
let batch = BatchCommand::new("Insert and replace".to_string())
.add_command(Box::new(InsertTextCommand::new(
Position::new(5),
" World".to_string(),
)))
.add_command(Box::new(ReplaceTextCommand::new(
Range::new(Position::new(0), Position::new(5)),
"Hi".to_string(),
)));
let result = batch.execute(&mut doc).unwrap();
assert!(result.success);
assert!(result.content_changed);
assert_eq!(doc.text(), "Hi World");
}
#[test]
fn fluent_api_usage() {
let mut doc = EditorDocument::new();
let result = doc.command().at(Position::new(0)).insert("Hello").unwrap();
assert!(result.success);
assert_eq!(doc.text(), "Hello");
let range = Range::new(Position::new(0), Position::new(5));
let result = doc.command().range(range).replace("Hi").unwrap();
assert!(result.success);
assert_eq!(doc.text(), "Hi");
}
#[test]
fn document_extension_methods() {
let mut doc = EditorDocument::new();
doc.insert_at(Position::new(0), "Hello").unwrap();
assert_eq!(doc.text(), "Hello");
let range = Range::new(Position::new(0), Position::new(5));
doc.replace_range(range, "Hi").unwrap();
assert_eq!(doc.text(), "Hi");
let range = Range::new(Position::new(0), Position::new(2));
doc.delete_range(range).unwrap();
assert_eq!(doc.text(), "");
}
#[test]
fn command_memory_usage() {
let insert_cmd = InsertTextCommand::new(Position::new(0), "Hello".to_string());
let usage = insert_cmd.memory_usage();
assert!(usage >= core::mem::size_of::<InsertTextCommand>() + 5);
}
}