Skip to main content

text_document/
document.rs

1//! TextDocument implementation.
2
3use std::sync::Arc;
4
5use parking_lot::Mutex;
6
7use anyhow::Result;
8use base64::Engine;
9use base64::engine::general_purpose::STANDARD as BASE64;
10
11use crate::{ResourceType, TextDirection, WrapMode};
12use frontend::commands::{
13    document_commands, document_inspection_commands, document_io_commands,
14    document_search_commands, resource_commands, undo_redo_commands,
15};
16
17use crate::convert::{self, to_i64, to_usize};
18use crate::cursor::TextCursor;
19use crate::events::{self, DocumentEvent, Subscription};
20use crate::flow::FormatChangeKind;
21use crate::inner::TextDocumentInner;
22use crate::operation::{DocxExportResult, HtmlImportResult, MarkdownImportResult, Operation};
23use crate::{BlockFormat, BlockInfo, DocumentStats, FindMatch, FindOptions};
24
25/// A rich text document.
26///
27/// Owns the backend (database, event hub, undo/redo manager) and provides
28/// document-level operations. All cursor-based editing goes through
29/// [`TextCursor`], obtained via [`cursor()`](TextDocument::cursor) or
30/// [`cursor_at()`](TextDocument::cursor_at).
31///
32/// Internally uses `Arc<Mutex<...>>` so that multiple [`TextCursor`]s can
33/// coexist and edit concurrently. Cloning a `TextDocument` creates a new
34/// handle to the **same** underlying document (like Qt's implicit sharing).
35#[derive(Clone)]
36pub struct TextDocument {
37    pub(crate) inner: Arc<Mutex<TextDocumentInner>>,
38}
39
40impl TextDocument {
41    // ── Construction ──────────────────────────────────────────
42
43    /// Create a new, empty document.
44    ///
45    /// # Panics
46    ///
47    /// Panics if the database context cannot be created (e.g. filesystem error).
48    /// Use [`TextDocument::try_new`] for a fallible alternative.
49    pub fn new() -> Self {
50        Self::try_new().expect("failed to initialize document")
51    }
52
53    /// Create a new, empty document, returning an error on failure.
54    pub fn try_new() -> Result<Self> {
55        let ctx = frontend::AppContext::new();
56        let doc_inner = TextDocumentInner::initialize(ctx)?;
57        let inner = Arc::new(Mutex::new(doc_inner));
58
59        // Bridge backend long-operation events to public DocumentEvent.
60        Self::subscribe_long_operation_events(&inner);
61
62        Ok(Self { inner })
63    }
64
65    /// Subscribe to backend long-operation events and bridge them to DocumentEvent.
66    fn subscribe_long_operation_events(inner: &Arc<Mutex<TextDocumentInner>>) {
67        use frontend::common::event::{LongOperationEvent as LOE, Origin};
68
69        let weak = Arc::downgrade(inner);
70        {
71            let locked = inner.lock();
72            // Progress
73            let w = weak.clone();
74            locked
75                .event_client
76                .subscribe(Origin::LongOperation(LOE::Progress), move |event| {
77                    if let Some(inner) = w.upgrade() {
78                        let (op_id, percent, message) = parse_progress_data(&event.data);
79                        let mut inner = inner.lock();
80                        inner.queue_event(DocumentEvent::LongOperationProgress {
81                            operation_id: op_id,
82                            percent,
83                            message,
84                        });
85                    }
86                });
87
88            // Completed
89            let w = weak.clone();
90            locked
91                .event_client
92                .subscribe(Origin::LongOperation(LOE::Completed), move |event| {
93                    if let Some(inner) = w.upgrade() {
94                        let op_id = parse_id_data(&event.data);
95                        let mut inner = inner.lock();
96                        inner.queue_event(DocumentEvent::DocumentReset);
97                        inner.check_block_count_changed();
98                        inner.reset_cached_child_order();
99                        inner.queue_event(DocumentEvent::LongOperationFinished {
100                            operation_id: op_id,
101                            success: true,
102                            error: None,
103                        });
104                    }
105                });
106
107            // Cancelled
108            let w = weak.clone();
109            locked
110                .event_client
111                .subscribe(Origin::LongOperation(LOE::Cancelled), move |event| {
112                    if let Some(inner) = w.upgrade() {
113                        let op_id = parse_id_data(&event.data);
114                        let mut inner = inner.lock();
115                        inner.queue_event(DocumentEvent::LongOperationFinished {
116                            operation_id: op_id,
117                            success: false,
118                            error: Some("cancelled".into()),
119                        });
120                    }
121                });
122
123            // Failed
124            locked
125                .event_client
126                .subscribe(Origin::LongOperation(LOE::Failed), move |event| {
127                    if let Some(inner) = weak.upgrade() {
128                        let (op_id, error) = parse_failed_data(&event.data);
129                        let mut inner = inner.lock();
130                        inner.queue_event(DocumentEvent::LongOperationFinished {
131                            operation_id: op_id,
132                            success: false,
133                            error: Some(error),
134                        });
135                    }
136                });
137        }
138    }
139
140    // ── Whole-document content ────────────────────────────────
141
142    /// Replace the entire document with plain text. Clears undo history.
143    pub fn set_plain_text(&self, text: &str) -> Result<()> {
144        let queued = {
145            let mut inner = self.inner.lock();
146            let dto = frontend::document_io::ImportPlainTextDto {
147                plain_text: text.into(),
148            };
149            document_io_commands::import_plain_text(&inner.ctx, &dto)?;
150            undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
151            inner.invalidate_text_cache();
152            inner.queue_event(DocumentEvent::DocumentReset);
153            inner.check_block_count_changed();
154            inner.reset_cached_child_order();
155            inner.queue_event(DocumentEvent::UndoRedoChanged {
156                can_undo: false,
157                can_redo: false,
158            });
159            inner.take_queued_events()
160        };
161        crate::inner::dispatch_queued_events(queued);
162        Ok(())
163    }
164
165    /// Export the entire document as plain text.
166    pub fn to_plain_text(&self) -> Result<String> {
167        let mut inner = self.inner.lock();
168        Ok(inner.plain_text()?.to_string())
169    }
170
171    /// Replace the entire document with Markdown. Clears undo history.
172    ///
173    /// This is a **long operation**. Returns a typed [`Operation`] handle.
174    pub fn set_markdown(&self, markdown: &str) -> Result<Operation<MarkdownImportResult>> {
175        let mut inner = self.inner.lock();
176        inner.invalidate_text_cache();
177        let dto = frontend::document_io::ImportMarkdownDto {
178            markdown_text: markdown.into(),
179        };
180        let op_id = document_io_commands::import_markdown(&inner.ctx, &dto)?;
181        Ok(Operation::new(
182            op_id,
183            &inner.ctx,
184            Box::new(|ctx, id| {
185                document_io_commands::get_import_markdown_result(ctx, id)
186                    .ok()
187                    .flatten()
188                    .map(|r| {
189                        Ok(MarkdownImportResult {
190                            block_count: to_usize(r.block_count),
191                        })
192                    })
193            }),
194        ))
195    }
196
197    /// Export the entire document as Markdown.
198    pub fn to_markdown(&self) -> Result<String> {
199        let inner = self.inner.lock();
200        let dto = document_io_commands::export_markdown(&inner.ctx)?;
201        Ok(dto.markdown_text)
202    }
203
204    /// Replace the entire document with HTML. Clears undo history.
205    ///
206    /// This is a **long operation**. Returns a typed [`Operation`] handle.
207    pub fn set_html(&self, html: &str) -> Result<Operation<HtmlImportResult>> {
208        let mut inner = self.inner.lock();
209        inner.invalidate_text_cache();
210        let dto = frontend::document_io::ImportHtmlDto {
211            html_text: html.into(),
212        };
213        let op_id = document_io_commands::import_html(&inner.ctx, &dto)?;
214        Ok(Operation::new(
215            op_id,
216            &inner.ctx,
217            Box::new(|ctx, id| {
218                document_io_commands::get_import_html_result(ctx, id)
219                    .ok()
220                    .flatten()
221                    .map(|r| {
222                        Ok(HtmlImportResult {
223                            block_count: to_usize(r.block_count),
224                        })
225                    })
226            }),
227        ))
228    }
229
230    /// Export the entire document as HTML.
231    pub fn to_html(&self) -> Result<String> {
232        let inner = self.inner.lock();
233        let dto = document_io_commands::export_html(&inner.ctx)?;
234        Ok(dto.html_text)
235    }
236
237    /// Export the entire document as LaTeX.
238    pub fn to_latex(&self, document_class: &str, include_preamble: bool) -> Result<String> {
239        let inner = self.inner.lock();
240        let dto = frontend::document_io::ExportLatexDto {
241            document_class: document_class.into(),
242            include_preamble,
243        };
244        let result = document_io_commands::export_latex(&inner.ctx, &dto)?;
245        Ok(result.latex_text)
246    }
247
248    /// Export the entire document as DOCX to a file path.
249    ///
250    /// This is a **long operation**. Returns a typed [`Operation`] handle.
251    pub fn to_docx(&self, output_path: &str) -> Result<Operation<DocxExportResult>> {
252        let inner = self.inner.lock();
253        let dto = frontend::document_io::ExportDocxDto {
254            output_path: output_path.into(),
255        };
256        let op_id = document_io_commands::export_docx(&inner.ctx, &dto)?;
257        Ok(Operation::new(
258            op_id,
259            &inner.ctx,
260            Box::new(|ctx, id| {
261                document_io_commands::get_export_docx_result(ctx, id)
262                    .ok()
263                    .flatten()
264                    .map(|r| {
265                        Ok(DocxExportResult {
266                            file_path: r.file_path,
267                            paragraph_count: to_usize(r.paragraph_count),
268                        })
269                    })
270            }),
271        ))
272    }
273
274    /// Clear all document content and reset to an empty state.
275    pub fn clear(&self) -> Result<()> {
276        let queued = {
277            let mut inner = self.inner.lock();
278            let dto = frontend::document_io::ImportPlainTextDto {
279                plain_text: String::new(),
280            };
281            document_io_commands::import_plain_text(&inner.ctx, &dto)?;
282            undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
283            inner.invalidate_text_cache();
284            inner.queue_event(DocumentEvent::DocumentReset);
285            inner.check_block_count_changed();
286            inner.reset_cached_child_order();
287            inner.queue_event(DocumentEvent::UndoRedoChanged {
288                can_undo: false,
289                can_redo: false,
290            });
291            inner.take_queued_events()
292        };
293        crate::inner::dispatch_queued_events(queued);
294        Ok(())
295    }
296
297    // ── Cursor factory ───────────────────────────────────────
298
299    /// Create a cursor at position 0.
300    pub fn cursor(&self) -> TextCursor {
301        self.cursor_at(0)
302    }
303
304    /// Create a cursor at the given position.
305    pub fn cursor_at(&self, position: usize) -> TextCursor {
306        let data = {
307            let mut inner = self.inner.lock();
308            inner.register_cursor(position)
309        };
310        TextCursor {
311            doc: self.inner.clone(),
312            data,
313        }
314    }
315
316    // ── Document queries ─────────────────────────────────────
317
318    /// Get document statistics. O(1) — reads cached values.
319    pub fn stats(&self) -> DocumentStats {
320        let inner = self.inner.lock();
321        let dto = document_inspection_commands::get_document_stats(&inner.ctx)
322            .expect("get_document_stats should not fail");
323        DocumentStats::from(&dto)
324    }
325
326    /// Get the total character count. O(1) — reads cached value.
327    pub fn character_count(&self) -> usize {
328        let inner = self.inner.lock();
329        let dto = document_inspection_commands::get_document_stats(&inner.ctx)
330            .expect("get_document_stats should not fail");
331        to_usize(dto.character_count)
332    }
333
334    /// Get the number of blocks (paragraphs). O(1) — reads cached value.
335    pub fn block_count(&self) -> usize {
336        let inner = self.inner.lock();
337        let dto = document_inspection_commands::get_document_stats(&inner.ctx)
338            .expect("get_document_stats should not fail");
339        to_usize(dto.block_count)
340    }
341
342    /// Returns true if the document has no text content.
343    pub fn is_empty(&self) -> bool {
344        self.character_count() == 0
345    }
346
347    /// Get text at a position for a given length.
348    pub fn text_at(&self, position: usize, length: usize) -> Result<String> {
349        let inner = self.inner.lock();
350        let dto = frontend::document_inspection::GetTextAtPositionDto {
351            position: to_i64(position),
352            length: to_i64(length),
353        };
354        let result = document_inspection_commands::get_text_at_position(&inner.ctx, &dto)?;
355        Ok(result.text)
356    }
357
358    /// Get info about the block at a position. O(log n).
359    pub fn block_at(&self, position: usize) -> Result<BlockInfo> {
360        let inner = self.inner.lock();
361        let dto = frontend::document_inspection::GetBlockAtPositionDto {
362            position: to_i64(position),
363        };
364        let result = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)?;
365        Ok(BlockInfo::from(&result))
366    }
367
368    /// Get the block format at a position.
369    pub fn block_format_at(&self, position: usize) -> Result<BlockFormat> {
370        let inner = self.inner.lock();
371        let dto = frontend::document_inspection::GetBlockAtPositionDto {
372            position: to_i64(position),
373        };
374        let block_info = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)?;
375        let block_id = block_info.block_id;
376        let block_id = block_id as u64;
377        let block_dto = frontend::commands::block_commands::get_block(&inner.ctx, &block_id)?
378            .ok_or_else(|| anyhow::anyhow!("block not found"))?;
379        Ok(BlockFormat::from(&block_dto))
380    }
381
382    // ── Flow traversal (layout engine API) ─────────────────
383
384    /// Walk the main frame's visual flow in document order.
385    ///
386    /// Returns the top-level flow elements — blocks, tables, and
387    /// sub-frames — in the order defined by the main frame's
388    /// `child_order`. Table cell contents are NOT included here;
389    /// access them through [`TextTableCell::blocks()`](crate::TextTableCell::blocks).
390    ///
391    /// This is the primary entry point for layout initialization.
392    pub fn flow(&self) -> Vec<crate::flow::FlowElement> {
393        let inner = self.inner.lock();
394        let main_frame_id = get_main_frame_id(&inner);
395        crate::text_frame::build_flow_elements(&inner, &self.inner, main_frame_id)
396    }
397
398    /// Get a read-only handle to a block by its entity ID.
399    ///
400    /// Entity IDs are stable across insertions and deletions.
401    /// Returns `None` if no block with this ID exists.
402    pub fn block_by_id(&self, block_id: usize) -> Option<crate::text_block::TextBlock> {
403        let inner = self.inner.lock();
404        let exists = frontend::commands::block_commands::get_block(&inner.ctx, &(block_id as u64))
405            .ok()
406            .flatten()
407            .is_some();
408
409        if exists {
410            Some(crate::text_block::TextBlock {
411                doc: self.inner.clone(),
412                block_id,
413            })
414        } else {
415            None
416        }
417    }
418
419    /// Get a read-only handle to the block containing the given
420    /// character position. Returns `None` if position is out of range.
421    pub fn block_at_position(&self, position: usize) -> Option<crate::text_block::TextBlock> {
422        let inner = self.inner.lock();
423        let dto = frontend::document_inspection::GetBlockAtPositionDto {
424            position: to_i64(position),
425        };
426        let result = document_inspection_commands::get_block_at_position(&inner.ctx, &dto).ok()?;
427        Some(crate::text_block::TextBlock {
428            doc: self.inner.clone(),
429            block_id: result.block_id as usize,
430        })
431    }
432
433    /// Get a read-only handle to a block by its 0-indexed global
434    /// block number.
435    ///
436    /// **O(n)**: requires scanning all blocks sorted by
437    /// `document_position` to find the nth one. Prefer
438    /// [`block_at_position()`](TextDocument::block_at_position) or
439    /// [`block_by_id()`](TextDocument::block_by_id) in
440    /// performance-sensitive paths.
441    pub fn block_by_number(&self, block_number: usize) -> Option<crate::text_block::TextBlock> {
442        let inner = self.inner.lock();
443        let all_blocks = frontend::commands::block_commands::get_all_block(&inner.ctx).ok()?;
444        let mut sorted: Vec<_> = all_blocks.into_iter().collect();
445        sorted.sort_by_key(|b| b.document_position);
446
447        sorted
448            .get(block_number)
449            .map(|b| crate::text_block::TextBlock {
450                doc: self.inner.clone(),
451                block_id: b.id as usize,
452            })
453    }
454
455    /// All blocks in the document, sorted by `document_position`. **O(n)**.
456    ///
457    /// Returns blocks from all frames, including those inside table cells.
458    /// This is the efficient way to iterate all blocks — avoids the O(n^2)
459    /// cost of calling `block_by_number(i)` in a loop.
460    pub fn blocks(&self) -> Vec<crate::text_block::TextBlock> {
461        let inner = self.inner.lock();
462        let all_blocks =
463            frontend::commands::block_commands::get_all_block(&inner.ctx).unwrap_or_default();
464        let mut sorted: Vec<_> = all_blocks.into_iter().collect();
465        sorted.sort_by_key(|b| b.document_position);
466        sorted
467            .iter()
468            .map(|b| crate::text_block::TextBlock {
469                doc: self.inner.clone(),
470                block_id: b.id as usize,
471            })
472            .collect()
473    }
474
475    /// All blocks whose character range intersects `[position, position + length)`.
476    ///
477    /// **O(n)**: scans all blocks once. Returns them sorted by `document_position`.
478    /// A block intersects if its range `[block.position, block.position + block.length)`
479    /// overlaps the query range. An empty query range (`length == 0`) returns the
480    /// block containing that position, if any.
481    pub fn blocks_in_range(
482        &self,
483        position: usize,
484        length: usize,
485    ) -> Vec<crate::text_block::TextBlock> {
486        let inner = self.inner.lock();
487        let all_blocks =
488            frontend::commands::block_commands::get_all_block(&inner.ctx).unwrap_or_default();
489        let mut sorted: Vec<_> = all_blocks.into_iter().collect();
490        sorted.sort_by_key(|b| b.document_position);
491
492        let range_start = position;
493        let range_end = position + length;
494
495        sorted
496            .iter()
497            .filter(|b| {
498                let block_start = b.document_position.max(0) as usize;
499                let block_end = block_start + b.text_length.max(0) as usize;
500                // Overlap check: block intersects [range_start, range_end)
501                if length == 0 {
502                    // Point query: block contains the position
503                    range_start >= block_start && range_start < block_end
504                } else {
505                    block_start < range_end && block_end > range_start
506                }
507            })
508            .map(|b| crate::text_block::TextBlock {
509                doc: self.inner.clone(),
510                block_id: b.id as usize,
511            })
512            .collect()
513    }
514
515    /// Snapshot the entire main flow in a single lock acquisition.
516    ///
517    /// Returns a [`FlowSnapshot`](crate::FlowSnapshot) containing snapshots
518    /// for every element in the flow.
519    pub fn snapshot_flow(&self) -> crate::flow::FlowSnapshot {
520        let inner = self.inner.lock();
521        let main_frame_id = get_main_frame_id(&inner);
522        let elements = crate::text_frame::build_flow_snapshot(&inner, main_frame_id);
523        crate::flow::FlowSnapshot { elements }
524    }
525
526    // ── Search ───────────────────────────────────────────────
527
528    /// Find the next (or previous) occurrence. Returns `None` if not found.
529    pub fn find(
530        &self,
531        query: &str,
532        from: usize,
533        options: &FindOptions,
534    ) -> Result<Option<FindMatch>> {
535        let inner = self.inner.lock();
536        let dto = options.to_find_text_dto(query, from);
537        let result = document_search_commands::find_text(&inner.ctx, &dto)?;
538        Ok(convert::find_result_to_match(&result))
539    }
540
541    /// Find all occurrences.
542    pub fn find_all(&self, query: &str, options: &FindOptions) -> Result<Vec<FindMatch>> {
543        let inner = self.inner.lock();
544        let dto = options.to_find_all_dto(query);
545        let result = document_search_commands::find_all(&inner.ctx, &dto)?;
546        Ok(convert::find_all_to_matches(&result))
547    }
548
549    /// Replace occurrences. Returns the number of replacements. Undoable.
550    pub fn replace_text(
551        &self,
552        query: &str,
553        replacement: &str,
554        replace_all: bool,
555        options: &FindOptions,
556    ) -> Result<usize> {
557        let (count, queued) = {
558            let mut inner = self.inner.lock();
559            let dto = options.to_replace_dto(query, replacement, replace_all);
560            let result =
561                document_search_commands::replace_text(&inner.ctx, Some(inner.stack_id), &dto)?;
562            let count = to_usize(result.replacements_count);
563            inner.invalidate_text_cache();
564            if count > 0 {
565                inner.modified = true;
566                // Replacements are scattered across the document — we can't
567                // provide a single position/chars delta. Signal "content changed
568                // from position 0, affecting `count` sites" so the consumer
569                // knows to re-read.
570                inner.queue_event(DocumentEvent::ContentsChanged {
571                    position: 0,
572                    chars_removed: 0,
573                    chars_added: 0,
574                    blocks_affected: count,
575                });
576                inner.check_block_count_changed();
577                inner.check_flow_changed();
578                let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
579                let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
580                inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
581            }
582            (count, inner.take_queued_events())
583        };
584        crate::inner::dispatch_queued_events(queued);
585        Ok(count)
586    }
587
588    // ── Resources ────────────────────────────────────────────
589
590    /// Add a resource (image, stylesheet) to the document.
591    pub fn add_resource(
592        &self,
593        resource_type: ResourceType,
594        name: &str,
595        mime_type: &str,
596        data: &[u8],
597    ) -> Result<()> {
598        let mut inner = self.inner.lock();
599        let dto = frontend::resource::dtos::CreateResourceDto {
600            created_at: Default::default(),
601            updated_at: Default::default(),
602            resource_type,
603            name: name.into(),
604            url: String::new(),
605            mime_type: mime_type.into(),
606            data_base64: BASE64.encode(data),
607        };
608        let created = resource_commands::create_resource(
609            &inner.ctx,
610            Some(inner.stack_id),
611            &dto,
612            inner.document_id,
613            -1,
614        )?;
615        inner.resource_cache.insert(name.to_string(), created.id);
616        Ok(())
617    }
618
619    /// Get a resource by name. Returns `None` if not found.
620    ///
621    /// Uses an internal cache to avoid scanning all resources on repeated lookups.
622    pub fn resource(&self, name: &str) -> Result<Option<Vec<u8>>> {
623        let mut inner = self.inner.lock();
624
625        // Fast path: check the name → ID cache.
626        if let Some(&id) = inner.resource_cache.get(name) {
627            if let Some(r) = resource_commands::get_resource(&inner.ctx, &id)? {
628                let bytes = BASE64.decode(&r.data_base64)?;
629                return Ok(Some(bytes));
630            }
631            // ID was stale — fall through to full scan.
632            inner.resource_cache.remove(name);
633        }
634
635        // Slow path: linear scan, then populate cache for the match.
636        let all = resource_commands::get_all_resource(&inner.ctx)?;
637        for r in &all {
638            if r.name == name {
639                inner.resource_cache.insert(name.to_string(), r.id);
640                let bytes = BASE64.decode(&r.data_base64)?;
641                return Ok(Some(bytes));
642            }
643        }
644        Ok(None)
645    }
646
647    // ── Undo / Redo ──────────────────────────────────────────
648
649    /// Undo the last operation.
650    pub fn undo(&self) -> Result<()> {
651        let queued = {
652            let mut inner = self.inner.lock();
653            let before = capture_block_state(&inner);
654            let result = undo_redo_commands::undo(&inner.ctx, Some(inner.stack_id));
655            inner.invalidate_text_cache();
656            result?;
657            emit_undo_redo_change_events(&mut inner, &before);
658            inner.check_block_count_changed();
659            inner.check_flow_changed();
660            let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
661            let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
662            inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
663            inner.take_queued_events()
664        };
665        crate::inner::dispatch_queued_events(queued);
666        Ok(())
667    }
668
669    /// Redo the last undone operation.
670    pub fn redo(&self) -> Result<()> {
671        let queued = {
672            let mut inner = self.inner.lock();
673            let before = capture_block_state(&inner);
674            let result = undo_redo_commands::redo(&inner.ctx, Some(inner.stack_id));
675            inner.invalidate_text_cache();
676            result?;
677            emit_undo_redo_change_events(&mut inner, &before);
678            inner.check_block_count_changed();
679            inner.check_flow_changed();
680            let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
681            let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
682            inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
683            inner.take_queued_events()
684        };
685        crate::inner::dispatch_queued_events(queued);
686        Ok(())
687    }
688
689    /// Returns true if there are operations that can be undone.
690    pub fn can_undo(&self) -> bool {
691        let inner = self.inner.lock();
692        undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id))
693    }
694
695    /// Returns true if there are operations that can be redone.
696    pub fn can_redo(&self) -> bool {
697        let inner = self.inner.lock();
698        undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id))
699    }
700
701    /// Clear all undo/redo history.
702    pub fn clear_undo_redo(&self) {
703        let inner = self.inner.lock();
704        undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
705    }
706
707    // ── Modified state ───────────────────────────────────────
708
709    /// Returns true if the document has been modified since creation or last reset.
710    pub fn is_modified(&self) -> bool {
711        self.inner.lock().modified
712    }
713
714    /// Set or clear the modified flag.
715    pub fn set_modified(&self, modified: bool) {
716        let queued = {
717            let mut inner = self.inner.lock();
718            if inner.modified != modified {
719                inner.modified = modified;
720                inner.queue_event(DocumentEvent::ModificationChanged(modified));
721            }
722            inner.take_queued_events()
723        };
724        crate::inner::dispatch_queued_events(queued);
725    }
726
727    // ── Document properties ──────────────────────────────────
728
729    /// Get the document title.
730    pub fn title(&self) -> String {
731        let inner = self.inner.lock();
732        document_commands::get_document(&inner.ctx, &inner.document_id)
733            .ok()
734            .flatten()
735            .map(|d| d.title)
736            .unwrap_or_default()
737    }
738
739    /// Set the document title.
740    pub fn set_title(&self, title: &str) -> Result<()> {
741        let inner = self.inner.lock();
742        let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
743            .ok_or_else(|| anyhow::anyhow!("document not found"))?;
744        let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
745        update.title = title.into();
746        document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
747        Ok(())
748    }
749
750    /// Get the text direction.
751    pub fn text_direction(&self) -> TextDirection {
752        let inner = self.inner.lock();
753        document_commands::get_document(&inner.ctx, &inner.document_id)
754            .ok()
755            .flatten()
756            .map(|d| d.text_direction)
757            .unwrap_or(TextDirection::LeftToRight)
758    }
759
760    /// Set the text direction.
761    pub fn set_text_direction(&self, direction: TextDirection) -> Result<()> {
762        let inner = self.inner.lock();
763        let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
764            .ok_or_else(|| anyhow::anyhow!("document not found"))?;
765        let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
766        update.text_direction = direction;
767        document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
768        Ok(())
769    }
770
771    /// Get the default wrap mode.
772    pub fn default_wrap_mode(&self) -> WrapMode {
773        let inner = self.inner.lock();
774        document_commands::get_document(&inner.ctx, &inner.document_id)
775            .ok()
776            .flatten()
777            .map(|d| d.default_wrap_mode)
778            .unwrap_or(WrapMode::WordWrap)
779    }
780
781    /// Set the default wrap mode.
782    pub fn set_default_wrap_mode(&self, mode: WrapMode) -> Result<()> {
783        let inner = self.inner.lock();
784        let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
785            .ok_or_else(|| anyhow::anyhow!("document not found"))?;
786        let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
787        update.default_wrap_mode = mode;
788        document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
789        Ok(())
790    }
791
792    // ── Event subscription ───────────────────────────────────
793
794    /// Subscribe to document events via callback.
795    ///
796    /// Callbacks are invoked **outside** the document lock (after the editing
797    /// operation completes and the lock is released). It is safe to call
798    /// `TextDocument` or `TextCursor` methods from within the callback without
799    /// risk of deadlock. However, keep callbacks lightweight — they run
800    /// synchronously on the calling thread and block the caller until they
801    /// return.
802    ///
803    /// Drop the returned [`Subscription`] to unsubscribe.
804    ///
805    /// # Breaking change (v0.0.6)
806    ///
807    /// The callback bound changed from `Send` to `Send + Sync` in v0.0.6
808    /// to support `Arc`-based dispatch. Callbacks that capture non-`Sync`
809    /// types (e.g., `Rc<T>`, `Cell<T>`) must be wrapped in a `Mutex`.
810    pub fn on_change<F>(&self, callback: F) -> Subscription
811    where
812        F: Fn(DocumentEvent) + Send + Sync + 'static,
813    {
814        let mut inner = self.inner.lock();
815        events::subscribe_inner(&mut inner, callback)
816    }
817
818    /// Return events accumulated since the last `poll_events()` call.
819    ///
820    /// This delivery path is independent of callback dispatch via
821    /// [`on_change`](Self::on_change) — using both simultaneously is safe
822    /// and each path sees every event exactly once.
823    pub fn poll_events(&self) -> Vec<DocumentEvent> {
824        let mut inner = self.inner.lock();
825        inner.drain_poll_events()
826    }
827}
828
829impl Default for TextDocument {
830    fn default() -> Self {
831        Self::new()
832    }
833}
834
835// ── Undo/redo change detection helpers ─────────────────────────
836
837/// Lightweight block state for before/after comparison during undo/redo.
838struct UndoBlockState {
839    id: u64,
840    position: i64,
841    text_length: i64,
842    plain_text: String,
843    format: BlockFormat,
844}
845
846/// Capture the state of all blocks, sorted by document_position.
847fn capture_block_state(inner: &TextDocumentInner) -> Vec<UndoBlockState> {
848    let all_blocks =
849        frontend::commands::block_commands::get_all_block(&inner.ctx).unwrap_or_default();
850    let mut states: Vec<UndoBlockState> = all_blocks
851        .into_iter()
852        .map(|b| UndoBlockState {
853            id: b.id,
854            position: b.document_position,
855            text_length: b.text_length,
856            plain_text: b.plain_text.clone(),
857            format: BlockFormat::from(&b),
858        })
859        .collect();
860    states.sort_by_key(|s| s.position);
861    states
862}
863
864/// Compare block state before and after undo/redo and emit
865/// ContentsChanged / FormatChanged events for affected regions.
866fn emit_undo_redo_change_events(inner: &mut TextDocumentInner, before: &[UndoBlockState]) {
867    let after = capture_block_state(inner);
868
869    // Build a map of block id → state for the "before" set.
870    let before_map: std::collections::HashMap<u64, &UndoBlockState> =
871        before.iter().map(|s| (s.id, s)).collect();
872    let after_map: std::collections::HashMap<u64, &UndoBlockState> =
873        after.iter().map(|s| (s.id, s)).collect();
874
875    // Track the affected content region (earliest position, total old/new length).
876    let mut content_changed = false;
877    let mut earliest_pos: Option<usize> = None;
878    let mut old_end: usize = 0;
879    let mut new_end: usize = 0;
880    let mut blocks_affected: usize = 0;
881
882    let mut format_only_changes: Vec<(usize, usize)> = Vec::new(); // (position, length)
883
884    // Check blocks present in both before and after.
885    for after_state in &after {
886        if let Some(before_state) = before_map.get(&after_state.id) {
887            let text_changed = before_state.plain_text != after_state.plain_text
888                || before_state.text_length != after_state.text_length;
889            let format_changed = before_state.format != after_state.format;
890
891            if text_changed {
892                content_changed = true;
893                blocks_affected += 1;
894                let pos = after_state.position.max(0) as usize;
895                earliest_pos = Some(earliest_pos.map_or(pos, |p: usize| p.min(pos)));
896                old_end = old_end.max(
897                    before_state.position.max(0) as usize
898                        + before_state.text_length.max(0) as usize,
899                );
900                new_end = new_end.max(pos + after_state.text_length.max(0) as usize);
901            } else if format_changed {
902                let pos = after_state.position.max(0) as usize;
903                let len = after_state.text_length.max(0) as usize;
904                format_only_changes.push((pos, len));
905            }
906        } else {
907            // Block exists in after but not in before — new block from undo/redo.
908            content_changed = true;
909            blocks_affected += 1;
910            let pos = after_state.position.max(0) as usize;
911            earliest_pos = Some(earliest_pos.map_or(pos, |p: usize| p.min(pos)));
912            new_end = new_end.max(pos + after_state.text_length.max(0) as usize);
913        }
914    }
915
916    // Check blocks that were removed (present in before but not after).
917    for before_state in before {
918        if !after_map.contains_key(&before_state.id) {
919            content_changed = true;
920            blocks_affected += 1;
921            let pos = before_state.position.max(0) as usize;
922            earliest_pos = Some(earliest_pos.map_or(pos, |p: usize| p.min(pos)));
923            old_end = old_end.max(pos + before_state.text_length.max(0) as usize);
924        }
925    }
926
927    if content_changed {
928        let position = earliest_pos.unwrap_or(0);
929        inner.queue_event(DocumentEvent::ContentsChanged {
930            position,
931            chars_removed: old_end.saturating_sub(position),
932            chars_added: new_end.saturating_sub(position),
933            blocks_affected,
934        });
935    }
936
937    // Emit FormatChanged for blocks where only formatting changed (not content).
938    for (position, length) in format_only_changes {
939        inner.queue_event(DocumentEvent::FormatChanged {
940            position,
941            length,
942            kind: FormatChangeKind::Block,
943        });
944    }
945}
946
947// ── Flow helpers ──────────────────────────────────────────────
948
949/// Get the main frame ID for the document.
950fn get_main_frame_id(inner: &TextDocumentInner) -> frontend::common::types::EntityId {
951    // The document's first frame is the main frame.
952    let frames = frontend::commands::document_commands::get_document_relationship(
953        &inner.ctx,
954        &inner.document_id,
955        &frontend::document::dtos::DocumentRelationshipField::Frames,
956    )
957    .unwrap_or_default();
958
959    frames.first().copied().unwrap_or(0)
960}
961
962// ── Long-operation event data helpers ─────────────────────────
963
964/// Parse progress JSON: `{"id":"...", "percentage": 50.0, "message": "..."}`
965fn parse_progress_data(data: &Option<String>) -> (String, f64, String) {
966    let Some(json) = data else {
967        return (String::new(), 0.0, String::new());
968    };
969    let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
970    let id = v["id"].as_str().unwrap_or_default().to_string();
971    let pct = v["percentage"].as_f64().unwrap_or(0.0);
972    let msg = v["message"].as_str().unwrap_or_default().to_string();
973    (id, pct, msg)
974}
975
976/// Parse completed/cancelled JSON: `{"id":"..."}`
977fn parse_id_data(data: &Option<String>) -> String {
978    let Some(json) = data else {
979        return String::new();
980    };
981    let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
982    v["id"].as_str().unwrap_or_default().to_string()
983}
984
985/// Parse failed JSON: `{"id":"...", "error":"..."}`
986fn parse_failed_data(data: &Option<String>) -> (String, String) {
987    let Some(json) = data else {
988        return (String::new(), "unknown error".into());
989    };
990    let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
991    let id = v["id"].as_str().unwrap_or_default().to_string();
992    let error = v["error"].as_str().unwrap_or("unknown error").to_string();
993    (id, error)
994}