use std::sync::Arc;
use parking_lot::Mutex;
use anyhow::Result;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use crate::{ResourceType, TextDirection, WrapMode};
use frontend::commands::{
block_commands, document_commands, document_inspection_commands, document_io_commands,
document_search_commands, frame_commands, resource_commands, table_cell_commands,
table_commands, undo_redo_commands,
};
use crate::convert::{self, to_i64, to_usize};
use crate::cursor::TextCursor;
use crate::events::{self, DocumentEvent, Subscription};
use crate::flow::FormatChangeKind;
use crate::inner::TextDocumentInner;
use crate::operation::{DocxExportResult, HtmlImportResult, MarkdownImportResult, Operation};
use crate::{BlockFormat, BlockInfo, DocumentStats, FindMatch, FindOptions};
#[derive(Clone)]
pub struct TextDocument {
pub(crate) inner: Arc<Mutex<TextDocumentInner>>,
}
impl TextDocument {
pub fn new() -> Self {
Self::try_new().expect("failed to initialize document")
}
pub fn try_new() -> Result<Self> {
let ctx = frontend::AppContext::new();
let doc_inner = TextDocumentInner::initialize(ctx)?;
let inner = Arc::new(Mutex::new(doc_inner));
Self::subscribe_long_operation_events(&inner);
Ok(Self { inner })
}
fn subscribe_long_operation_events(inner: &Arc<Mutex<TextDocumentInner>>) {
use frontend::common::event::{LongOperationEvent as LOE, Origin};
let weak = Arc::downgrade(inner);
let mut locked = inner.lock();
let w = weak.clone();
let progress_tok =
locked
.event_client
.subscribe(Origin::LongOperation(LOE::Progress), move |event| {
if let Some(inner) = w.upgrade() {
let (op_id, percent, message) = parse_progress_data(&event.data);
let mut inner = inner.lock();
inner.queue_event(DocumentEvent::LongOperationProgress {
operation_id: op_id,
percent,
message,
});
}
});
let w = weak.clone();
let completed_tok =
locked
.event_client
.subscribe(Origin::LongOperation(LOE::Completed), move |event| {
if let Some(inner) = w.upgrade() {
let op_id = parse_id_data(&event.data);
let mut inner = inner.lock();
inner.queue_event(DocumentEvent::DocumentReset);
inner.check_block_count_changed();
inner.reset_cached_child_order();
inner.queue_event(DocumentEvent::LongOperationFinished {
operation_id: op_id,
success: true,
error: None,
});
}
});
let w = weak.clone();
let cancelled_tok =
locked
.event_client
.subscribe(Origin::LongOperation(LOE::Cancelled), move |event| {
if let Some(inner) = w.upgrade() {
let op_id = parse_id_data(&event.data);
let mut inner = inner.lock();
inner.queue_event(DocumentEvent::LongOperationFinished {
operation_id: op_id,
success: false,
error: Some("cancelled".into()),
});
}
});
let failed_tok =
locked
.event_client
.subscribe(Origin::LongOperation(LOE::Failed), move |event| {
if let Some(inner) = weak.upgrade() {
let (op_id, error) = parse_failed_data(&event.data);
let mut inner = inner.lock();
inner.queue_event(DocumentEvent::LongOperationFinished {
operation_id: op_id,
success: false,
error: Some(error),
});
}
});
locked.long_op_subscriptions.extend([
progress_tok,
completed_tok,
cancelled_tok,
failed_tok,
]);
}
pub fn set_plain_text(&self, text: &str) -> Result<()> {
let queued = {
let mut inner = self.inner.lock();
let dto = frontend::document_io::ImportPlainTextDto {
plain_text: text.into(),
};
document_io_commands::import_plain_text(&inner.ctx, &dto)?;
undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
inner.invalidate_text_cache();
inner.rehighlight_all();
inner.queue_event(DocumentEvent::DocumentReset);
inner.check_block_count_changed();
inner.reset_cached_child_order();
inner.queue_event(DocumentEvent::UndoRedoChanged {
can_undo: false,
can_redo: false,
});
inner.take_queued_events()
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn to_plain_text(&self) -> Result<String> {
let mut inner = self.inner.lock();
Ok(inner.plain_text()?.to_string())
}
pub fn set_markdown(&self, markdown: &str) -> Result<Operation<MarkdownImportResult>> {
let mut inner = self.inner.lock();
inner.invalidate_text_cache();
let dto = frontend::document_io::ImportMarkdownDto {
markdown_text: markdown.into(),
};
let op_id = document_io_commands::import_markdown(&inner.ctx, &dto)?;
Ok(Operation::new(
op_id,
&inner.ctx,
Box::new(|ctx, id| {
document_io_commands::get_import_markdown_result(ctx, id)
.ok()
.flatten()
.map(|r| {
Ok(MarkdownImportResult {
block_count: to_usize(r.block_count),
})
})
}),
))
}
pub fn to_markdown(&self) -> Result<String> {
let inner = self.inner.lock();
let dto = document_io_commands::export_markdown(&inner.ctx)?;
Ok(dto.markdown_text)
}
pub fn set_html(&self, html: &str) -> Result<Operation<HtmlImportResult>> {
let mut inner = self.inner.lock();
inner.invalidate_text_cache();
let dto = frontend::document_io::ImportHtmlDto {
html_text: html.into(),
};
let op_id = document_io_commands::import_html(&inner.ctx, &dto)?;
Ok(Operation::new(
op_id,
&inner.ctx,
Box::new(|ctx, id| {
document_io_commands::get_import_html_result(ctx, id)
.ok()
.flatten()
.map(|r| {
Ok(HtmlImportResult {
block_count: to_usize(r.block_count),
})
})
}),
))
}
pub fn to_html(&self) -> Result<String> {
let inner = self.inner.lock();
let dto = document_io_commands::export_html(&inner.ctx)?;
Ok(dto.html_text)
}
pub fn to_latex(&self, document_class: &str, include_preamble: bool) -> Result<String> {
let inner = self.inner.lock();
let dto = frontend::document_io::ExportLatexDto {
document_class: document_class.into(),
include_preamble,
};
let result = document_io_commands::export_latex(&inner.ctx, &dto)?;
Ok(result.latex_text)
}
pub fn to_docx(&self, output_path: &str) -> Result<Operation<DocxExportResult>> {
let inner = self.inner.lock();
let dto = frontend::document_io::ExportDocxDto {
output_path: output_path.into(),
};
let op_id = document_io_commands::export_docx(&inner.ctx, &dto)?;
Ok(Operation::new(
op_id,
&inner.ctx,
Box::new(|ctx, id| {
document_io_commands::get_export_docx_result(ctx, id)
.ok()
.flatten()
.map(|r| {
Ok(DocxExportResult {
file_path: r.file_path,
paragraph_count: to_usize(r.paragraph_count),
})
})
}),
))
}
pub fn clear(&self) -> Result<()> {
let queued = {
let mut inner = self.inner.lock();
let dto = frontend::document_io::ImportPlainTextDto {
plain_text: String::new(),
};
document_io_commands::import_plain_text(&inner.ctx, &dto)?;
undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
inner.invalidate_text_cache();
inner.rehighlight_all();
inner.queue_event(DocumentEvent::DocumentReset);
inner.check_block_count_changed();
inner.reset_cached_child_order();
inner.queue_event(DocumentEvent::UndoRedoChanged {
can_undo: false,
can_redo: false,
});
inner.take_queued_events()
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn cursor(&self) -> TextCursor {
self.cursor_at(0)
}
pub fn cursor_at(&self, position: usize) -> TextCursor {
let data = {
let mut inner = self.inner.lock();
inner.register_cursor(position)
};
TextCursor {
doc: self.inner.clone(),
data,
}
}
pub fn stats(&self) -> DocumentStats {
let inner = self.inner.lock();
let dto = document_inspection_commands::get_document_stats(&inner.ctx)
.expect("get_document_stats should not fail");
DocumentStats::from(&dto)
}
pub fn character_count(&self) -> usize {
let inner = self.inner.lock();
let dto = document_inspection_commands::get_document_stats(&inner.ctx)
.expect("get_document_stats should not fail");
to_usize(dto.character_count)
}
pub fn block_count(&self) -> usize {
let inner = self.inner.lock();
let dto = document_inspection_commands::get_document_stats(&inner.ctx)
.expect("get_document_stats should not fail");
to_usize(dto.block_count)
}
pub fn is_empty(&self) -> bool {
self.character_count() == 0
}
pub fn text_at(&self, position: usize, length: usize) -> Result<String> {
let inner = self.inner.lock();
let dto = frontend::document_inspection::GetTextAtPositionDto {
position: to_i64(position),
length: to_i64(length),
};
let result = document_inspection_commands::get_text_at_position(&inner.ctx, &dto)?;
Ok(result.text)
}
pub fn find_element_at_position(&self, position: usize) -> Option<(u64, usize, usize)> {
let block_info = self.block_at(position).ok()?;
let block_start = block_info.start;
let offset_in_block = position.checked_sub(block_start)?;
let block = crate::text_block::TextBlock {
doc: std::sync::Arc::clone(&self.inner),
block_id: block_info.block_id,
};
let frags = block.fragments();
let mut last_text: Option<(u64, usize, usize, usize)> = None; for frag in &frags {
match frag {
crate::flow::FragmentContent::Text {
offset,
length,
element_id,
..
} => {
let frag_start = *offset;
let frag_end = frag_start + *length;
if offset_in_block >= frag_start && offset_in_block < frag_end {
let abs_start = block_start + frag_start;
let offset_within = offset_in_block - frag_start;
return Some((*element_id, abs_start, offset_within));
}
if offset_in_block == frag_end {
last_text =
Some((*element_id, block_start + frag_start, frag_start, *length));
}
}
crate::flow::FragmentContent::Image {
offset, element_id, ..
} => {
if offset_in_block == *offset {
return Some((*element_id, block_start + offset, 0));
}
}
}
}
last_text.map(|(id, abs_start, _, length)| (id, abs_start, length))
}
pub fn block_at(&self, position: usize) -> Result<BlockInfo> {
let inner = self.inner.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(position),
};
let result = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)?;
Ok(BlockInfo::from(&result))
}
pub fn block_format_at(&self, position: usize) -> Result<BlockFormat> {
let inner = self.inner.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(position),
};
let block_info = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)?;
let block_id = block_info.block_id;
let block_id = block_id as u64;
let block_dto = frontend::commands::block_commands::get_block(&inner.ctx, &block_id)?
.ok_or_else(|| anyhow::anyhow!("block not found"))?;
Ok(BlockFormat::from(&block_dto))
}
pub fn flow(&self) -> Vec<crate::flow::FlowElement> {
let inner = self.inner.lock();
let main_frame_id = get_main_frame_id(&inner);
crate::text_frame::build_flow_elements(&inner, &self.inner, main_frame_id)
}
pub fn block_by_id(&self, block_id: usize) -> Option<crate::text_block::TextBlock> {
let inner = self.inner.lock();
let exists = frontend::commands::block_commands::get_block(&inner.ctx, &(block_id as u64))
.ok()
.flatten()
.is_some();
if exists {
Some(crate::text_block::TextBlock {
doc: self.inner.clone(),
block_id,
})
} else {
None
}
}
pub fn snapshot_block_at_position(
&self,
position: usize,
) -> Option<crate::flow::BlockSnapshot> {
let inner = self.inner.lock();
let main_frame_id = get_main_frame_id(&inner);
let ordered_block_ids = collect_frame_block_ids(&inner, main_frame_id)?;
let pos = position as i64;
let mut running_pos: i64 = 0;
for &block_id in &ordered_block_ids {
let block_dto = block_commands::get_block(&inner.ctx, &block_id)
.ok()
.flatten()?;
let block_end = running_pos + block_dto.text_length;
if pos >= running_pos && pos <= block_end {
return crate::text_block::build_block_snapshot_with_position(
&inner,
block_id,
Some(running_pos as usize),
);
}
running_pos = block_end + 1;
}
if let Some(&last_id) = ordered_block_ids.last() {
return crate::text_block::build_block_snapshot(&inner, last_id);
}
None
}
pub fn block_at_position(&self, position: usize) -> Option<crate::text_block::TextBlock> {
let inner = self.inner.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(position),
};
let result = document_inspection_commands::get_block_at_position(&inner.ctx, &dto).ok()?;
Some(crate::text_block::TextBlock {
doc: self.inner.clone(),
block_id: result.block_id as usize,
})
}
pub fn block_by_number(&self, block_number: usize) -> Option<crate::text_block::TextBlock> {
let inner = self.inner.lock();
let all_blocks = frontend::commands::block_commands::get_all_block(&inner.ctx).ok()?;
let mut sorted: Vec<_> = all_blocks.into_iter().collect();
sorted.sort_by_key(|b| b.document_position);
sorted
.get(block_number)
.map(|b| crate::text_block::TextBlock {
doc: self.inner.clone(),
block_id: b.id as usize,
})
}
pub fn blocks(&self) -> Vec<crate::text_block::TextBlock> {
let inner = self.inner.lock();
let all_blocks =
frontend::commands::block_commands::get_all_block(&inner.ctx).unwrap_or_default();
let mut sorted: Vec<_> = all_blocks.into_iter().collect();
sorted.sort_by_key(|b| b.document_position);
sorted
.iter()
.map(|b| crate::text_block::TextBlock {
doc: self.inner.clone(),
block_id: b.id as usize,
})
.collect()
}
pub fn blocks_in_range(
&self,
position: usize,
length: usize,
) -> Vec<crate::text_block::TextBlock> {
let inner = self.inner.lock();
let all_blocks =
frontend::commands::block_commands::get_all_block(&inner.ctx).unwrap_or_default();
let mut sorted: Vec<_> = all_blocks.into_iter().collect();
sorted.sort_by_key(|b| b.document_position);
let range_start = position;
let range_end = position + length;
sorted
.iter()
.filter(|b| {
let block_start = b.document_position.max(0) as usize;
let block_end = block_start + b.text_length.max(0) as usize;
if length == 0 {
range_start >= block_start && range_start < block_end
} else {
block_start < range_end && block_end > range_start
}
})
.map(|b| crate::text_block::TextBlock {
doc: self.inner.clone(),
block_id: b.id as usize,
})
.collect()
}
pub fn snapshot_flow(&self) -> crate::flow::FlowSnapshot {
let inner = self.inner.lock();
let main_frame_id = get_main_frame_id(&inner);
let elements = crate::text_frame::build_flow_snapshot(&inner, main_frame_id);
crate::flow::FlowSnapshot { elements }
}
pub fn find(
&self,
query: &str,
from: usize,
options: &FindOptions,
) -> Result<Option<FindMatch>> {
let inner = self.inner.lock();
let dto = options.to_find_text_dto(query, from);
let result = document_search_commands::find_text(&inner.ctx, &dto)?;
Ok(convert::find_result_to_match(&result))
}
pub fn find_all(&self, query: &str, options: &FindOptions) -> Result<Vec<FindMatch>> {
let inner = self.inner.lock();
let dto = options.to_find_all_dto(query);
let result = document_search_commands::find_all(&inner.ctx, &dto)?;
Ok(convert::find_all_to_matches(&result))
}
pub fn replace_text(
&self,
query: &str,
replacement: &str,
replace_all: bool,
options: &FindOptions,
) -> Result<usize> {
let (count, queued) = {
let mut inner = self.inner.lock();
let dto = options.to_replace_dto(query, replacement, replace_all);
let result =
document_search_commands::replace_text(&inner.ctx, Some(inner.stack_id), &dto)?;
let count = to_usize(result.replacements_count);
inner.invalidate_text_cache();
if count > 0 {
inner.modified = true;
inner.rehighlight_all();
inner.queue_event(DocumentEvent::ContentsChanged {
position: 0,
chars_removed: 0,
chars_added: 0,
blocks_affected: count,
});
inner.check_block_count_changed();
inner.check_flow_changed();
let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
}
(count, inner.take_queued_events())
};
crate::inner::dispatch_queued_events(queued);
Ok(count)
}
pub fn add_resource(
&self,
resource_type: ResourceType,
name: &str,
mime_type: &str,
data: &[u8],
) -> Result<()> {
let mut inner = self.inner.lock();
let dto = frontend::resource::dtos::CreateResourceDto {
created_at: Default::default(),
updated_at: Default::default(),
resource_type,
name: name.into(),
url: String::new(),
mime_type: mime_type.into(),
data_base64: BASE64.encode(data),
};
let created = resource_commands::create_resource(
&inner.ctx,
Some(inner.stack_id),
&dto,
inner.document_id,
-1,
)?;
inner.resource_cache.insert(name.to_string(), created.id);
Ok(())
}
pub fn resource(&self, name: &str) -> Result<Option<Vec<u8>>> {
let mut inner = self.inner.lock();
if let Some(&id) = inner.resource_cache.get(name) {
if let Some(r) = resource_commands::get_resource(&inner.ctx, &id)? {
let bytes = BASE64.decode(&r.data_base64)?;
return Ok(Some(bytes));
}
inner.resource_cache.remove(name);
}
let all = resource_commands::get_all_resource(&inner.ctx)?;
for r in &all {
if r.name == name {
inner.resource_cache.insert(name.to_string(), r.id);
let bytes = BASE64.decode(&r.data_base64)?;
return Ok(Some(bytes));
}
}
Ok(None)
}
pub fn undo(&self) -> Result<()> {
let queued = {
let mut inner = self.inner.lock();
let before = capture_block_state(&inner);
let result = undo_redo_commands::undo(&inner.ctx, Some(inner.stack_id));
inner.invalidate_text_cache();
result?;
inner.rehighlight_all();
emit_undo_redo_change_events(&mut inner, &before);
inner.check_block_count_changed();
inner.check_flow_changed();
let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
inner.take_queued_events()
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn redo(&self) -> Result<()> {
let queued = {
let mut inner = self.inner.lock();
let before = capture_block_state(&inner);
let result = undo_redo_commands::redo(&inner.ctx, Some(inner.stack_id));
inner.invalidate_text_cache();
result?;
inner.rehighlight_all();
emit_undo_redo_change_events(&mut inner, &before);
inner.check_block_count_changed();
inner.check_flow_changed();
let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
inner.take_queued_events()
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn can_undo(&self) -> bool {
let inner = self.inner.lock();
undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id))
}
pub fn can_redo(&self) -> bool {
let inner = self.inner.lock();
undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id))
}
pub fn clear_undo_redo(&self) {
let inner = self.inner.lock();
undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
}
pub fn is_modified(&self) -> bool {
self.inner.lock().modified
}
pub fn set_modified(&self, modified: bool) {
let queued = {
let mut inner = self.inner.lock();
if inner.modified != modified {
inner.modified = modified;
inner.queue_event(DocumentEvent::ModificationChanged(modified));
}
inner.take_queued_events()
};
crate::inner::dispatch_queued_events(queued);
}
pub fn title(&self) -> String {
let inner = self.inner.lock();
document_commands::get_document(&inner.ctx, &inner.document_id)
.ok()
.flatten()
.map(|d| d.title)
.unwrap_or_default()
}
pub fn set_title(&self, title: &str) -> Result<()> {
let inner = self.inner.lock();
let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
.ok_or_else(|| anyhow::anyhow!("document not found"))?;
let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
update.title = title.into();
document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
Ok(())
}
pub fn text_direction(&self) -> TextDirection {
let inner = self.inner.lock();
document_commands::get_document(&inner.ctx, &inner.document_id)
.ok()
.flatten()
.map(|d| d.text_direction)
.unwrap_or(TextDirection::LeftToRight)
}
pub fn set_text_direction(&self, direction: TextDirection) -> Result<()> {
let inner = self.inner.lock();
let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
.ok_or_else(|| anyhow::anyhow!("document not found"))?;
let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
update.text_direction = direction;
document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
Ok(())
}
pub fn default_wrap_mode(&self) -> WrapMode {
let inner = self.inner.lock();
document_commands::get_document(&inner.ctx, &inner.document_id)
.ok()
.flatten()
.map(|d| d.default_wrap_mode)
.unwrap_or(WrapMode::WordWrap)
}
pub fn set_default_wrap_mode(&self, mode: WrapMode) -> Result<()> {
let inner = self.inner.lock();
let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
.ok_or_else(|| anyhow::anyhow!("document not found"))?;
let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
update.default_wrap_mode = mode;
document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
Ok(())
}
pub fn on_change<F>(&self, callback: F) -> Subscription
where
F: Fn(DocumentEvent) + Send + Sync + 'static,
{
let mut inner = self.inner.lock();
events::subscribe_inner(&mut inner, callback)
}
pub fn poll_events(&self) -> Vec<DocumentEvent> {
let mut inner = self.inner.lock();
inner.drain_poll_events()
}
pub fn set_syntax_highlighter(&self, highlighter: Option<Arc<dyn crate::SyntaxHighlighter>>) {
let mut inner = self.inner.lock();
match highlighter {
Some(hl) => {
inner.highlight = Some(crate::highlight::HighlightData {
highlighter: hl,
blocks: std::collections::HashMap::new(),
});
inner.rehighlight_all();
}
None => {
inner.highlight = None;
}
}
}
pub fn rehighlight(&self) {
let mut inner = self.inner.lock();
inner.rehighlight_all();
}
pub fn rehighlight_block(&self, block_id: usize) {
let mut inner = self.inner.lock();
inner.rehighlight_from_block(block_id);
}
}
impl Default for TextDocument {
fn default() -> Self {
Self::new()
}
}
struct UndoBlockState {
id: u64,
position: i64,
text_length: i64,
plain_text: String,
format: BlockFormat,
}
fn capture_block_state(inner: &TextDocumentInner) -> Vec<UndoBlockState> {
let all_blocks =
frontend::commands::block_commands::get_all_block(&inner.ctx).unwrap_or_default();
let mut states: Vec<UndoBlockState> = all_blocks
.into_iter()
.map(|b| UndoBlockState {
id: b.id,
position: b.document_position,
text_length: b.text_length,
plain_text: b.plain_text.clone(),
format: BlockFormat::from(&b),
})
.collect();
states.sort_by_key(|s| s.position);
states
}
fn build_doc_text(states: &[UndoBlockState]) -> String {
states
.iter()
.map(|s| s.plain_text.as_str())
.collect::<Vec<_>>()
.join("\n")
}
fn compute_text_edit(before: &str, after: &str) -> (usize, usize, usize) {
let before_chars: Vec<char> = before.chars().collect();
let after_chars: Vec<char> = after.chars().collect();
let prefix_len = before_chars
.iter()
.zip(after_chars.iter())
.take_while(|(a, b)| a == b)
.count();
let before_remaining = before_chars.len() - prefix_len;
let after_remaining = after_chars.len() - prefix_len;
let suffix_len = before_chars
.iter()
.rev()
.zip(after_chars.iter().rev())
.take(before_remaining.min(after_remaining))
.take_while(|(a, b)| a == b)
.count();
let removed = before_remaining - suffix_len;
let added = after_remaining - suffix_len;
(prefix_len, removed, added)
}
fn emit_undo_redo_change_events(inner: &mut TextDocumentInner, before: &[UndoBlockState]) {
let after = capture_block_state(inner);
let before_map: std::collections::HashMap<u64, &UndoBlockState> =
before.iter().map(|s| (s.id, s)).collect();
let after_map: std::collections::HashMap<u64, &UndoBlockState> =
after.iter().map(|s| (s.id, s)).collect();
let mut content_changed = false;
let mut earliest_pos: Option<usize> = None;
let mut old_end: usize = 0;
let mut new_end: usize = 0;
let mut blocks_affected: usize = 0;
let mut format_only_changes: Vec<(usize, usize)> = Vec::new();
for after_state in &after {
if let Some(before_state) = before_map.get(&after_state.id) {
let text_changed = before_state.plain_text != after_state.plain_text
|| before_state.text_length != after_state.text_length;
let format_changed = before_state.format != after_state.format;
if text_changed {
content_changed = true;
blocks_affected += 1;
let pos = after_state.position.max(0) as usize;
earliest_pos = Some(earliest_pos.map_or(pos, |p: usize| p.min(pos)));
old_end = old_end.max(
before_state.position.max(0) as usize
+ before_state.text_length.max(0) as usize,
);
new_end = new_end.max(pos + after_state.text_length.max(0) as usize);
} else if format_changed {
let pos = after_state.position.max(0) as usize;
let len = after_state.text_length.max(0) as usize;
format_only_changes.push((pos, len));
}
} else {
content_changed = true;
blocks_affected += 1;
let pos = after_state.position.max(0) as usize;
earliest_pos = Some(earliest_pos.map_or(pos, |p: usize| p.min(pos)));
new_end = new_end.max(pos + after_state.text_length.max(0) as usize);
}
}
for before_state in before {
if !after_map.contains_key(&before_state.id) {
content_changed = true;
blocks_affected += 1;
let pos = before_state.position.max(0) as usize;
earliest_pos = Some(earliest_pos.map_or(pos, |p: usize| p.min(pos)));
old_end = old_end.max(pos + before_state.text_length.max(0) as usize);
}
}
if content_changed {
let position = earliest_pos.unwrap_or(0);
let chars_removed = old_end.saturating_sub(position);
let chars_added = new_end.saturating_sub(position);
let before_text = build_doc_text(before);
let after_text = build_doc_text(&after);
let (edit_offset, precise_removed, precise_added) =
compute_text_edit(&before_text, &after_text);
if precise_removed > 0 || precise_added > 0 {
inner.adjust_cursors(edit_offset, precise_removed, precise_added);
}
inner.queue_event(DocumentEvent::ContentsChanged {
position,
chars_removed,
chars_added,
blocks_affected,
});
}
for (position, length) in format_only_changes {
inner.queue_event(DocumentEvent::FormatChanged {
position,
length,
kind: FormatChangeKind::Block,
});
}
}
fn collect_frame_block_ids(
inner: &TextDocumentInner,
frame_id: frontend::common::types::EntityId,
) -> Option<Vec<u64>> {
let frame_dto = frame_commands::get_frame(&inner.ctx, &frame_id)
.ok()
.flatten()?;
if !frame_dto.child_order.is_empty() {
let mut block_ids = Vec::new();
for &entry in &frame_dto.child_order {
if entry > 0 {
block_ids.push(entry as u64);
} else if entry < 0 {
let sub_frame_id = (-entry) as u64;
let sub_frame = frame_commands::get_frame(&inner.ctx, &sub_frame_id)
.ok()
.flatten();
if let Some(ref sf) = sub_frame {
if let Some(table_id) = sf.table {
if let Some(table_dto) = table_commands::get_table(&inner.ctx, &table_id)
.ok()
.flatten()
{
let mut cell_dtos: Vec<_> = table_dto
.cells
.iter()
.filter_map(|&cid| {
table_cell_commands::get_table_cell(&inner.ctx, &cid)
.ok()
.flatten()
})
.collect();
cell_dtos
.sort_by(|a, b| a.row.cmp(&b.row).then(a.column.cmp(&b.column)));
for cell_dto in &cell_dtos {
if let Some(cf_id) = cell_dto.cell_frame
&& let Some(cf_ids) = collect_frame_block_ids(inner, cf_id)
{
block_ids.extend(cf_ids);
}
}
}
} else if let Some(sub_ids) = collect_frame_block_ids(inner, sub_frame_id) {
block_ids.extend(sub_ids);
}
}
}
}
Some(block_ids)
} else {
Some(frame_dto.blocks.to_vec())
}
}
pub(crate) fn get_main_frame_id(inner: &TextDocumentInner) -> frontend::common::types::EntityId {
let frames = frontend::commands::document_commands::get_document_relationship(
&inner.ctx,
&inner.document_id,
&frontend::document::dtos::DocumentRelationshipField::Frames,
)
.unwrap_or_default();
frames.first().copied().unwrap_or(0)
}
fn parse_progress_data(data: &Option<String>) -> (String, f64, String) {
let Some(json) = data else {
return (String::new(), 0.0, String::new());
};
let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
let id = v["id"].as_str().unwrap_or_default().to_string();
let pct = v["percentage"].as_f64().unwrap_or(0.0);
let msg = v["message"].as_str().unwrap_or_default().to_string();
(id, pct, msg)
}
fn parse_id_data(data: &Option<String>) -> String {
let Some(json) = data else {
return String::new();
};
let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
v["id"].as_str().unwrap_or_default().to_string()
}
fn parse_failed_data(data: &Option<String>) -> (String, String) {
let Some(json) = data else {
return (String::new(), "unknown error".into());
};
let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
let id = v["id"].as_str().unwrap_or_default().to_string();
let error = v["error"].as_str().unwrap_or("unknown error").to_string();
(id, error)
}