use super::editing_helpers::{
collect_block_ids_recursive, find_block_at_position, find_element_at_offset,
is_word_boundary_punct,
};
use crate::InsertTextDto;
use crate::InsertTextResultDto;
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;
use std::time::Instant;
pub trait InsertTextUnitOfWorkFactoryTrait: Send + Sync {
fn create(&self) -> Box<dyn InsertTextUnitOfWorkTrait>;
}
#[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 = "InlineElement", action = "Get")]
#[macros::uow_action(entity = "InlineElement", action = "GetMulti")]
#[macros::uow_action(entity = "InlineElement", action = "Update")]
#[macros::uow_action(entity = "Table", action = "GetRelationship")]
#[macros::uow_action(entity = "TableCell", action = "GetMulti")]
pub trait InsertTextUnitOfWorkTrait: CommandUnitOfWork {}
struct UndoData {
original_element: InlineElement,
original_block: Block,
doc_id: EntityId,
original_character_count: i64,
}
enum InsertTextUndo {
Simple(Box<UndoData>),
SelectionReplacement(common::snapshot::EntityTreeSnapshot),
}
fn delete_range_in_block(
uow: &mut Box<dyn InsertTextUnitOfWorkTrait>,
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_insert_with_selection(
uow: &mut Box<dyn InsertTextUnitOfWorkTrait>,
dto: &InsertTextDto,
) -> Result<(InsertTextResultDto, InsertTextUndo)> {
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 all_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,
)?;
let blocks_opt = uow.get_block_multi(&all_block_ids)?;
let mut blocks: Vec<Block> = blocks_opt.into_iter().flatten().collect();
blocks.sort_by_key(|b| b.document_position);
let sel_start = std::cmp::min(dto.position, dto.anchor);
let sel_end = std::cmp::max(dto.position, dto.anchor);
let position = sel_start;
let (sel_block, sel_block_idx, sel_start_offset) = find_block_at_position(&blocks, sel_start)?;
let (_, sel_end_block_idx, sel_end_offset) = find_block_at_position(&blocks, sel_end)?;
if sel_block_idx != sel_end_block_idx {
return Err(anyhow!(
"Cross-block selection replacement is not supported by insert_text. \
Use delete_text first, then insert_text."
));
}
let chars_removed = delete_range_in_block(uow, &sel_block, sel_start_offset, sel_end_offset)?;
document.character_count -= chars_removed;
document.updated_at = chrono::Utc::now();
uow.update_document(&document)?;
let shift = chars_removed;
let mut to_update = Vec::new();
for b in &blocks[(sel_block_idx + 1)..] {
let mut ub = b.clone();
ub.document_position -= shift;
ub.updated_at = chrono::Utc::now();
to_update.push(ub);
}
if !to_update.is_empty() {
uow.update_block_multi(&to_update)?;
}
let blocks_opt = uow.get_block_multi(&all_block_ids)?;
blocks = blocks_opt.into_iter().flatten().collect();
blocks.sort_by_key(|b| b.document_position);
document = uow
.get_document(&doc_id)?
.ok_or_else(|| anyhow!("Document not found"))?;
let (block, block_idx, offset) = find_block_at_position(&blocks, position)?;
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 mut updated_element = element.clone();
match &updated_element.content {
InlineContent::Text(s) => {
let mut new_text = s.clone();
let byte_offset = char_to_byte_offset(&new_text, elem_offset);
new_text.insert_str(byte_offset, &dto.text);
updated_element.content = InlineContent::Text(new_text);
}
InlineContent::Empty => {
updated_element.content = InlineContent::Text(dto.text.clone());
}
InlineContent::Image { .. } => {
return Err(anyhow!("Cannot insert text into an image element"));
}
}
updated_element.updated_at = chrono::Utc::now();
uow.update_inline_element(&updated_element)?;
let text_len = dto.text.chars().count() as i64;
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 = chrono::Utc::now();
uow.update_block(&updated_block)?;
let mut blocks_to_update: Vec<Block> = Vec::new();
for b in &blocks[(block_idx + 1)..] {
let mut ub = b.clone();
ub.document_position += text_len;
ub.updated_at = chrono::Utc::now();
blocks_to_update.push(ub);
}
if !blocks_to_update.is_empty() {
uow.update_block_multi(&blocks_to_update)?;
}
let mut updated_doc = document.clone();
updated_doc.character_count += text_len;
updated_doc.updated_at = chrono::Utc::now();
uow.update_document(&updated_doc)?;
Ok((
InsertTextResultDto {
new_position: block.document_position + offset + text_len,
blocks_affected: 1,
},
InsertTextUndo::SelectionReplacement(snapshot),
))
}
fn execute_insert_simple(
uow: &mut Box<dyn InsertTextUnitOfWorkTrait>,
dto: &InsertTextDto,
) -> Result<(InsertTextResultDto, InsertTextUndo)> {
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).clamp(0, block.text_length);
let original_block = block.clone();
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 original_element = element.clone();
let mut updated_element = element.clone();
match &updated_element.content {
InlineContent::Text(s) => {
let mut new_text = s.clone();
let byte_offset = char_to_byte_offset(&new_text, elem_offset);
new_text.insert_str(byte_offset, &dto.text);
updated_element.content = InlineContent::Text(new_text);
}
InlineContent::Empty => {
updated_element.content = InlineContent::Text(dto.text.clone());
}
InlineContent::Image { .. } => {
return Err(anyhow!("Cannot insert text into an image element"));
}
}
updated_element.updated_at = chrono::Utc::now();
uow.update_inline_element(&updated_element)?;
let text_len = dto.text.chars().count() as i64;
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 = chrono::Utc::now();
uow.update_block(&updated_block)?;
let mut updated_doc = document.clone();
updated_doc.character_count += text_len;
updated_doc.updated_at = chrono::Utc::now();
uow.update_document(&updated_doc)?;
let undo_data = UndoData {
original_element,
original_block,
doc_id,
original_character_count: document.character_count,
};
Ok((
InsertTextResultDto {
new_position: block_pos + offset + text_len,
blocks_affected: 1,
},
InsertTextUndo::Simple(Box::new(undo_data)),
))
}
fn find_block_at_position_sequential(
uow: &dyn InsertTextUnitOfWorkTrait,
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))
}
fn char_to_byte_offset(s: &str, char_offset: i64) -> usize {
let mut idx = 0;
let mut count = 0i64;
for (ci, ch) in s.char_indices() {
if count == char_offset {
return ci;
}
count += 1;
idx = ci + ch.len_utf8();
}
if count < char_offset { s.len() } else { idx }
}
pub struct InsertTextUseCase {
uow_factory: Box<dyn InsertTextUnitOfWorkFactoryTrait>,
undo_data: Option<InsertTextUndo>,
last_dto: Option<InsertTextDto>,
last_result: Option<InsertTextResultDto>,
last_merge_time: Option<Instant>,
was_selection_replacement: bool,
}
impl InsertTextUseCase {
pub fn new(uow_factory: Box<dyn InsertTextUnitOfWorkFactoryTrait>) -> Self {
InsertTextUseCase {
uow_factory,
undo_data: None,
last_dto: None,
last_result: None,
last_merge_time: None,
was_selection_replacement: false,
}
}
pub fn execute(&mut self, dto: &InsertTextDto) -> Result<InsertTextResultDto> {
let mut uow = self.uow_factory.create();
uow.begin_transaction()?;
let has_selection = dto.position != dto.anchor;
let (result, undo) = if has_selection {
execute_insert_with_selection(&mut uow, dto)?
} else {
execute_insert_simple(&mut uow, dto)?
};
self.undo_data = Some(undo);
self.last_dto = Some(dto.clone());
self.last_result = Some(result.clone());
self.last_merge_time = Some(Instant::now());
self.was_selection_replacement = has_selection;
uow.commit()?;
Ok(result)
}
}
impl UndoRedoCommand for InsertTextUseCase {
fn undo(&mut self) -> Result<()> {
let undo = self
.undo_data
.as_ref()
.ok_or_else(|| anyhow!("No undo data available"))?;
let mut uow = self.uow_factory.create();
uow.begin_transaction()?;
match undo {
InsertTextUndo::SelectionReplacement(snapshot) => {
uow.restore_document(&snapshot.clone())?;
}
InsertTextUndo::Simple(data) => {
uow.update_inline_element(&data.original_element)?;
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)?;
}
}
uow.commit()?;
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) = if has_selection {
execute_insert_with_selection(&mut uow, &dto)?
} else {
execute_insert_simple(&mut uow, &dto)?
};
self.undo_data = Some(undo);
uow.commit()?;
Ok(())
}
fn can_merge(&self, other: &dyn UndoRedoCommand) -> bool {
let Some(other_cmd) = other.as_any().downcast_ref::<InsertTextUseCase>() else {
return false;
};
let (Some(self_result), Some(self_time), Some(self_dto)) =
(&self.last_result, &self.last_merge_time, &self.last_dto)
else {
return false;
};
let (Some(other_dto), Some(other_time)) = (&other_cmd.last_dto, &other_cmd.last_merge_time)
else {
return false;
};
if other_time.duration_since(*self_time) > std::time::Duration::from_secs(2) {
return false;
}
if other_cmd.was_selection_replacement {
return false;
}
if other_dto.position != self_result.new_position {
return false;
}
if self_dto.text.chars().count() + other_dto.text.chars().count() > 200 {
return false;
}
let self_text = &self_dto.text;
let other_text = &other_dto.text;
if let (Some(last_self), Some(first_other)) =
(self_text.chars().next_back(), other_text.chars().next())
{
let self_is_boundary = last_self.is_whitespace() || is_word_boundary_punct(last_self);
let other_is_word =
!first_other.is_whitespace() && !is_word_boundary_punct(first_other);
if self_is_boundary && other_is_word {
return false;
}
}
true
}
fn merge(&mut self, other: &dyn UndoRedoCommand) -> bool {
let Some(other_cmd) = other.as_any().downcast_ref::<InsertTextUseCase>() else {
return false;
};
if let (Some(self_dto), Some(other_dto)) = (&mut self.last_dto, &other_cmd.last_dto) {
self_dto.text.push_str(&other_dto.text);
self_dto.anchor = self_dto.position; }
if let Some(other_result) = &other_cmd.last_result {
self.last_result = Some(other_result.clone());
}
self.last_merge_time = other_cmd.last_merge_time;
true
}
fn as_any(&self) -> &dyn Any {
self
}
}