use super::editing_helpers::{collect_block_ids_recursive, find_element_at_offset};
use crate::InsertFormattedTextDto;
use crate::InsertFormattedTextResultDto;
use anyhow::{Result, anyhow};
use common::database::CommandUnitOfWork;
use common::direct_access::block::block_repository::BlockRelationshipField;
use common::direct_access::document::document_repository::DocumentRelationshipField;
use common::direct_access::root::root_repository::RootRelationshipField;
use common::direct_access::table::TableRelationshipField;
use common::entities::{Block, Document, Frame, InlineContent, InlineElement, Root, TableCell};
use common::types::{EntityId, ROOT_ENTITY_ID};
use common::undo_redo::UndoRedoCommand;
use std::any::Any;
pub trait InsertFormattedTextUnitOfWorkFactoryTrait: Send + Sync {
fn create(&self) -> Box<dyn InsertFormattedTextUnitOfWorkTrait>;
}
#[macros::uow_action(entity = "Root", action = "Get")]
#[macros::uow_action(entity = "Root", action = "GetRelationship")]
#[macros::uow_action(entity = "Document", action = "Get")]
#[macros::uow_action(entity = "Document", action = "Update")]
#[macros::uow_action(entity = "Document", action = "GetRelationship")]
#[macros::uow_action(entity = "Document", action = "Snapshot")]
#[macros::uow_action(entity = "Document", action = "Restore")]
#[macros::uow_action(entity = "Frame", action = "Get")]
#[macros::uow_action(entity = "Frame", action = "GetRelationship")]
#[macros::uow_action(entity = "Block", action = "Get")]
#[macros::uow_action(entity = "Block", action = "GetMulti")]
#[macros::uow_action(entity = "Block", action = "Update")]
#[macros::uow_action(entity = "Block", action = "UpdateMulti")]
#[macros::uow_action(entity = "Block", action = "GetRelationship")]
#[macros::uow_action(entity = "Block", action = "SetRelationship")]
#[macros::uow_action(entity = "InlineElement", action = "Get")]
#[macros::uow_action(entity = "InlineElement", action = "GetMulti")]
#[macros::uow_action(entity = "InlineElement", action = "Update")]
#[macros::uow_action(entity = "InlineElement", action = "Create")]
#[macros::uow_action(entity = "InlineElement", action = "Remove")]
#[macros::uow_action(entity = "Table", action = "GetRelationship")]
#[macros::uow_action(entity = "TableCell", action = "GetMulti")]
pub trait InsertFormattedTextUnitOfWorkTrait: CommandUnitOfWork {}
struct SimpleUndoData {
original_element: InlineElement,
original_element_ids: Vec<EntityId>,
created_element_ids: Vec<EntityId>,
original_block: Block,
doc_id: EntityId,
original_character_count: i64,
block_id: EntityId,
}
enum InsertFormattedTextUndo {
Simple(Box<SimpleUndoData>),
SelectionReplacement(common::snapshot::EntityTreeSnapshot),
}
pub struct InsertFormattedTextUseCase {
uow_factory: Box<dyn InsertFormattedTextUnitOfWorkFactoryTrait>,
undo_data: Option<InsertFormattedTextUndo>,
last_dto: Option<InsertFormattedTextDto>,
}
fn delete_range_in_block(
uow: &mut Box<dyn InsertFormattedTextUnitOfWorkTrait>,
block: &Block,
start_offset: i64,
end_offset: i64,
) -> Result<i64> {
let element_ids = uow.get_block_relationship(&block.id, &BlockRelationshipField::Elements)?;
let elements_opt = uow.get_inline_element_multi(&element_ids)?;
let elements: Vec<InlineElement> = elements_opt.into_iter().flatten().collect();
let mut new_plain_text = String::new();
let mut new_text_length: i64 = 0;
let mut running: i64 = 0;
for elem in &elements {
let elem_len = match &elem.content {
InlineContent::Text(s) => s.chars().count() as i64,
InlineContent::Image { .. } => 1,
InlineContent::Empty => 0,
};
let elem_start = running;
let elem_end = running + elem_len;
let overlap_start = std::cmp::max(start_offset, elem_start);
let overlap_end = std::cmp::min(end_offset, elem_end);
if overlap_start < overlap_end {
let local_start = (overlap_start - elem_start) as usize;
let local_end = (overlap_end - elem_start) as usize;
match &elem.content {
InlineContent::Text(s) => {
let chars: Vec<char> = s.chars().collect();
let new_text: String = chars[..local_start]
.iter()
.chain(chars[local_end..].iter())
.collect();
new_plain_text.push_str(&new_text);
new_text_length += new_text.chars().count() as i64;
let mut updated = elem.clone();
updated.content = InlineContent::Text(new_text);
updated.updated_at = chrono::Utc::now();
uow.update_inline_element(&updated)?;
}
InlineContent::Image { .. } => {
let mut updated = elem.clone();
updated.content = InlineContent::Empty;
updated.updated_at = chrono::Utc::now();
uow.update_inline_element(&updated)?;
}
InlineContent::Empty => {}
}
} else {
match &elem.content {
InlineContent::Text(s) => {
new_plain_text.push_str(s);
new_text_length += s.chars().count() as i64;
}
InlineContent::Image { .. } => {
new_text_length += 1;
}
InlineContent::Empty => {}
}
}
running += elem_len;
}
let positions_removed = block.text_length - new_text_length;
let mut updated_block = block.clone();
updated_block.plain_text = new_plain_text;
updated_block.text_length = new_text_length;
updated_block.updated_at = chrono::Utc::now();
uow.update_block(&updated_block)?;
Ok(positions_removed)
}
fn execute_with_selection(
uow: &mut Box<dyn InsertFormattedTextUnitOfWorkTrait>,
dto: &InsertFormattedTextDto,
) -> Result<(InsertFormattedTextResultDto, InsertFormattedTextUndo)> {
let root = uow
.get_root(&ROOT_ENTITY_ID)?
.ok_or_else(|| anyhow!("Root entity not found"))?;
let doc_ids = uow.get_root_relationship(&root.id, &RootRelationshipField::Document)?;
let doc_id = *doc_ids
.first()
.ok_or_else(|| anyhow!("Root has no document"))?;
let mut document = uow
.get_document(&doc_id)?
.ok_or_else(|| anyhow!("Document not found"))?;
let snapshot = uow.snapshot_document(&[doc_id])?;
let frame_ids = uow.get_document_relationship(&doc_id, &DocumentRelationshipField::Frames)?;
let frame_id = *frame_ids
.first()
.ok_or_else(|| anyhow!("Document has no frames"))?;
let get_table_cell_frames = |table_id: &EntityId| -> anyhow::Result<Vec<EntityId>> {
let cell_ids = uow.get_table_relationship(table_id, &TableRelationshipField::Cells)?;
let cells_opt = uow.get_table_cell_multi(&cell_ids)?;
let mut cells: Vec<_> = cells_opt.into_iter().flatten().collect();
cells.sort_by(|a, b| a.row.cmp(&b.row).then(a.column.cmp(&b.column)));
Ok(cells.into_iter().filter_map(|c| c.cell_frame).collect())
};
let ordered_block_ids = collect_block_ids_recursive(
&|id| uow.get_frame(id),
&|id, field| uow.get_frame_relationship(id, field),
&get_table_cell_frames,
&frame_id,
)?;
if ordered_block_ids.is_empty() {
return Err(anyhow!("No blocks in document"));
}
let sel_start = std::cmp::min(dto.position, dto.anchor);
let sel_end = std::cmp::max(dto.position, dto.anchor);
let (sel_block, sel_block_idx, sel_block_pos) =
find_block_at_position_sequential(&**uow, &ordered_block_ids, sel_start)?;
let (_end_block, sel_end_block_idx, _) =
find_block_at_position_sequential(&**uow, &ordered_block_ids, sel_end)?;
if sel_block_idx != sel_end_block_idx {
return Err(anyhow!(
"Cross-block selection replacement is not supported by insert_formatted_text. \
Use delete_text first, then insert_formatted_text."
));
}
let start_offset = sel_start - sel_block_pos;
let end_offset = sel_end - sel_block_pos;
let chars_removed = delete_range_in_block(uow, &sel_block, start_offset, end_offset)?;
document.character_count -= chars_removed;
document.updated_at = chrono::Utc::now();
uow.update_document(&document)?;
let block = uow
.get_block(&sel_block.id)?
.ok_or_else(|| anyhow!("Block not found after deletion"))?;
let offset = sel_start - sel_block_pos;
let element_ids = uow.get_block_relationship(&block.id, &BlockRelationshipField::Elements)?;
let elements_opt = uow.get_inline_element_multi(&element_ids)?;
let elements: Vec<InlineElement> = elements_opt.into_iter().flatten().collect();
if elements.is_empty() {
return Err(anyhow!("Block has no inline elements"));
}
let (element, elem_idx, elem_offset) = find_element_at_offset(&elements, offset)?;
let now = chrono::Utc::now();
let text_len = dto.text.chars().count() as i64;
match &element.content {
InlineContent::Text(s) => {
let chars: Vec<char> = s.chars().collect();
let before_text: String = chars[..elem_offset as usize].iter().collect();
let after_text: String = chars[elem_offset as usize..].iter().collect();
let mut updated = element.clone();
updated.content = InlineContent::Text(before_text);
updated.updated_at = now;
uow.update_inline_element(&updated)?;
let new_elem = InlineElement {
id: 0,
created_at: now,
updated_at: now,
content: InlineContent::Text(dto.text.clone()),
fmt_font_family: Some(dto.font_family.clone()),
fmt_font_point_size: Some(dto.font_point_size),
fmt_font_bold: Some(dto.font_bold),
fmt_font_italic: Some(dto.font_italic),
fmt_font_underline: Some(dto.font_underline),
fmt_font_strikeout: Some(dto.font_strikeout),
..Default::default()
};
let insert_index = (elem_idx + 1) as i32;
uow.create_inline_element(&new_elem, block.id, insert_index)?;
if !after_text.is_empty() {
let after_elem = InlineElement {
id: 0,
created_at: now,
updated_at: now,
content: InlineContent::Text(after_text),
fmt_font_family: element.fmt_font_family.clone(),
fmt_font_point_size: element.fmt_font_point_size,
fmt_font_bold: element.fmt_font_bold,
fmt_font_italic: element.fmt_font_italic,
fmt_font_underline: element.fmt_font_underline,
fmt_font_overline: element.fmt_font_overline,
fmt_font_strikeout: element.fmt_font_strikeout,
fmt_font_weight: element.fmt_font_weight,
fmt_letter_spacing: element.fmt_letter_spacing,
fmt_word_spacing: element.fmt_word_spacing,
fmt_anchor_href: element.fmt_anchor_href.clone(),
fmt_anchor_names: element.fmt_anchor_names.clone(),
fmt_is_anchor: element.fmt_is_anchor,
fmt_tooltip: element.fmt_tooltip.clone(),
fmt_underline_style: element.fmt_underline_style.clone(),
fmt_vertical_alignment: element.fmt_vertical_alignment.clone(),
};
uow.create_inline_element(&after_elem, block.id, insert_index + 1)?;
}
}
InlineContent::Empty => {
let mut updated = element.clone();
updated.content = InlineContent::Text(dto.text.clone());
updated.fmt_font_family = Some(dto.font_family.clone());
updated.fmt_font_point_size = Some(dto.font_point_size);
updated.fmt_font_bold = Some(dto.font_bold);
updated.fmt_font_italic = Some(dto.font_italic);
updated.fmt_font_underline = Some(dto.font_underline);
updated.fmt_font_strikeout = Some(dto.font_strikeout);
updated.updated_at = now;
uow.update_inline_element(&updated)?;
}
InlineContent::Image { .. } => {
return Err(anyhow!("Cannot insert text into an image element"));
}
}
let mut updated_block = block.clone();
updated_block.text_length += text_len;
let byte_pos = char_to_byte_offset(&updated_block.plain_text, offset);
let mut plain = updated_block.plain_text.clone();
plain.insert_str(byte_pos, &dto.text);
updated_block.plain_text = plain;
updated_block.updated_at = now;
uow.update_block(&updated_block)?;
let mut updated_doc = uow
.get_document(&doc_id)?
.ok_or_else(|| anyhow!("Document not found"))?;
updated_doc.character_count += text_len;
updated_doc.updated_at = now;
uow.update_document(&updated_doc)?;
Ok((
InsertFormattedTextResultDto {
new_position: sel_start + text_len,
},
InsertFormattedTextUndo::SelectionReplacement(snapshot),
))
}
fn execute_insert_simple(
uow: &mut Box<dyn InsertFormattedTextUnitOfWorkTrait>,
dto: &InsertFormattedTextDto,
) -> Result<(InsertFormattedTextResultDto, InsertFormattedTextUndo)> {
let position = dto.position;
let root = uow
.get_root(&ROOT_ENTITY_ID)?
.ok_or_else(|| anyhow!("Root entity not found"))?;
let doc_ids = uow.get_root_relationship(&root.id, &RootRelationshipField::Document)?;
let doc_id = *doc_ids
.first()
.ok_or_else(|| anyhow!("Root has no document"))?;
let document = uow
.get_document(&doc_id)?
.ok_or_else(|| anyhow!("Document not found"))?;
let frame_ids = uow.get_document_relationship(&doc_id, &DocumentRelationshipField::Frames)?;
let frame_id = *frame_ids
.first()
.ok_or_else(|| anyhow!("Document has no frames"))?;
let get_table_cell_frames = |table_id: &EntityId| -> anyhow::Result<Vec<EntityId>> {
let cell_ids = uow.get_table_relationship(table_id, &TableRelationshipField::Cells)?;
let cells_opt = uow.get_table_cell_multi(&cell_ids)?;
let mut cells: Vec<_> = cells_opt.into_iter().flatten().collect();
cells.sort_by(|a, b| a.row.cmp(&b.row).then(a.column.cmp(&b.column)));
Ok(cells.into_iter().filter_map(|c| c.cell_frame).collect())
};
let ordered_block_ids = collect_block_ids_recursive(
&|id| uow.get_frame(id),
&|id, field| uow.get_frame_relationship(id, field),
&get_table_cell_frames,
&frame_id,
)?;
if ordered_block_ids.is_empty() {
return Err(anyhow!("No blocks in document"));
}
let (block, _block_idx, block_pos) =
find_block_at_position_sequential(&**uow, &ordered_block_ids, position)?;
let offset = position - block_pos;
let original_block = block.clone();
let element_ids = uow.get_block_relationship(&block.id, &BlockRelationshipField::Elements)?;
let original_element_ids = element_ids.clone();
let elements_opt = uow.get_inline_element_multi(&element_ids)?;
let elements: Vec<InlineElement> = elements_opt.into_iter().flatten().collect();
if elements.is_empty() {
return Err(anyhow!("Block has no inline elements"));
}
let (element, elem_idx, elem_offset) = find_element_at_offset(&elements, offset)?;
let original_element = element.clone();
let now = chrono::Utc::now();
let text_len = dto.text.chars().count() as i64;
let mut created_element_ids: Vec<EntityId> = Vec::new();
match &element.content {
InlineContent::Text(s) => {
let chars: Vec<char> = s.chars().collect();
let before_text: String = chars[..elem_offset as usize].iter().collect();
let after_text: String = chars[elem_offset as usize..].iter().collect();
let mut updated = element.clone();
updated.content = InlineContent::Text(before_text);
updated.updated_at = now;
uow.update_inline_element(&updated)?;
let new_elem = InlineElement {
id: 0,
created_at: now,
updated_at: now,
content: InlineContent::Text(dto.text.clone()),
fmt_font_family: Some(dto.font_family.clone()),
fmt_font_point_size: Some(dto.font_point_size),
fmt_font_bold: Some(dto.font_bold),
fmt_font_italic: Some(dto.font_italic),
fmt_font_underline: Some(dto.font_underline),
fmt_font_strikeout: Some(dto.font_strikeout),
..Default::default()
};
let insert_index = (elem_idx + 1) as i32;
let created = uow.create_inline_element(&new_elem, block.id, insert_index)?;
created_element_ids.push(created.id);
if !after_text.is_empty() {
let after_elem = InlineElement {
id: 0,
created_at: now,
updated_at: now,
content: InlineContent::Text(after_text),
fmt_font_family: element.fmt_font_family.clone(),
fmt_font_point_size: element.fmt_font_point_size,
fmt_font_bold: element.fmt_font_bold,
fmt_font_italic: element.fmt_font_italic,
fmt_font_underline: element.fmt_font_underline,
fmt_font_overline: element.fmt_font_overline,
fmt_font_strikeout: element.fmt_font_strikeout,
fmt_font_weight: element.fmt_font_weight,
fmt_letter_spacing: element.fmt_letter_spacing,
fmt_word_spacing: element.fmt_word_spacing,
fmt_anchor_href: element.fmt_anchor_href.clone(),
fmt_anchor_names: element.fmt_anchor_names.clone(),
fmt_is_anchor: element.fmt_is_anchor,
fmt_tooltip: element.fmt_tooltip.clone(),
fmt_underline_style: element.fmt_underline_style.clone(),
fmt_vertical_alignment: element.fmt_vertical_alignment.clone(),
};
let created_after =
uow.create_inline_element(&after_elem, block.id, insert_index + 1)?;
created_element_ids.push(created_after.id);
}
}
InlineContent::Empty => {
let mut updated = element.clone();
updated.content = InlineContent::Text(dto.text.clone());
updated.fmt_font_family = Some(dto.font_family.clone());
updated.fmt_font_point_size = Some(dto.font_point_size);
updated.fmt_font_bold = Some(dto.font_bold);
updated.fmt_font_italic = Some(dto.font_italic);
updated.fmt_font_underline = Some(dto.font_underline);
updated.fmt_font_strikeout = Some(dto.font_strikeout);
updated.updated_at = now;
uow.update_inline_element(&updated)?;
}
InlineContent::Image { .. } => {
return Err(anyhow!("Cannot insert text into an image element"));
}
}
let mut updated_block = block.clone();
updated_block.text_length += text_len;
let mut plain = updated_block.plain_text.clone();
let byte_pos = char_to_byte_offset(&plain, offset);
plain.insert_str(byte_pos, &dto.text);
updated_block.plain_text = plain;
updated_block.updated_at = now;
uow.update_block(&updated_block)?;
let mut updated_doc = document.clone();
updated_doc.character_count += text_len;
updated_doc.updated_at = now;
uow.update_document(&updated_doc)?;
let undo_data = SimpleUndoData {
original_element,
original_element_ids,
created_element_ids,
original_block,
doc_id,
original_character_count: document.character_count,
block_id: block.id,
};
Ok((
InsertFormattedTextResultDto {
new_position: position + text_len,
},
InsertFormattedTextUndo::Simple(Box::new(undo_data)),
))
}
fn char_to_byte_offset(s: &str, char_offset: i64) -> usize {
let mut idx = 0;
let mut char_count = 0i64;
for (ci, ch) in s.char_indices() {
if char_count == char_offset {
return ci;
}
char_count += 1;
idx = ci + ch.len_utf8();
}
if char_count < char_offset {
s.len()
} else {
idx
}
}
fn find_block_at_position_sequential(
uow: &dyn InsertFormattedTextUnitOfWorkTrait,
ordered_block_ids: &[EntityId],
position: i64,
) -> Result<(Block, usize, i64)> {
if ordered_block_ids.is_empty() {
return Err(anyhow!("No blocks in document"));
}
let mut running_pos: i64 = 0;
for (idx, &block_id) in ordered_block_ids.iter().enumerate() {
let block = uow
.get_block(&block_id)?
.ok_or_else(|| anyhow!("Block not found"))?;
let block_end = running_pos + block.text_length;
if position >= running_pos && position <= block_end {
return Ok((block, idx, running_pos));
}
running_pos = block_end + 1;
}
let last_idx = ordered_block_ids.len() - 1;
let block = uow
.get_block(&ordered_block_ids[last_idx])?
.ok_or_else(|| anyhow!("Block not found"))?;
let mut pos: i64 = 0;
for &id in &ordered_block_ids[..last_idx] {
if let Some(b) = uow.get_block(&id)? {
pos += b.text_length + 1;
}
}
Ok((block, last_idx, pos))
}
impl InsertFormattedTextUseCase {
pub fn new(uow_factory: Box<dyn InsertFormattedTextUnitOfWorkFactoryTrait>) -> Self {
InsertFormattedTextUseCase {
uow_factory,
undo_data: None,
last_dto: None,
}
}
pub fn execute(
&mut self,
dto: &InsertFormattedTextDto,
) -> Result<InsertFormattedTextResultDto> {
let mut uow = self.uow_factory.create();
uow.begin_transaction()?;
let has_selection = dto.position != dto.anchor;
let (result, undo_data) = if has_selection {
execute_with_selection(&mut uow, dto)?
} else {
execute_insert_simple(&mut uow, dto)?
};
self.undo_data = Some(undo_data);
self.last_dto = Some(dto.clone());
uow.commit()?;
Ok(result)
}
}
impl UndoRedoCommand for InsertFormattedTextUseCase {
fn undo(&mut self) -> Result<()> {
let undo_data = self
.undo_data
.take()
.ok_or_else(|| anyhow!("No undo data available"))?;
let mut uow = self.uow_factory.create();
uow.begin_transaction()?;
match &undo_data {
InsertFormattedTextUndo::Simple(data) => {
for &elem_id in &data.created_element_ids {
uow.remove_inline_element(&elem_id)?;
}
uow.update_inline_element(&data.original_element)?;
uow.set_block_relationship(
&data.block_id,
&BlockRelationshipField::Elements,
&data.original_element_ids,
)?;
uow.update_block(&data.original_block)?;
let mut doc = uow
.get_document(&data.doc_id)?
.ok_or_else(|| anyhow!("Document not found"))?;
doc.character_count = data.original_character_count;
doc.updated_at = chrono::Utc::now();
uow.update_document(&doc)?;
}
InsertFormattedTextUndo::SelectionReplacement(snapshot) => {
uow.restore_document(snapshot)?;
}
}
uow.commit()?;
self.undo_data = Some(undo_data);
Ok(())
}
fn redo(&mut self) -> Result<()> {
let dto = self
.last_dto
.as_ref()
.ok_or_else(|| anyhow!("No DTO available for redo"))?
.clone();
let mut uow = self.uow_factory.create();
uow.begin_transaction()?;
let has_selection = dto.position != dto.anchor;
let (_, undo_data) = if has_selection {
execute_with_selection(&mut uow, &dto)?
} else {
execute_insert_simple(&mut uow, &dto)?
};
self.undo_data = Some(undo_data);
uow.commit()?;
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
}