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    block_commands, document_commands, document_inspection_commands, document_io_commands,
14    document_search_commands, frame_commands, resource_commands, table_cell_commands,
15    table_commands, undo_redo_commands,
16};
17
18use crate::convert::{self, to_i64, to_usize};
19use crate::cursor::TextCursor;
20use crate::events::{self, DocumentEvent, Subscription};
21use crate::flow::FormatChangeKind;
22use crate::inner::TextDocumentInner;
23use crate::operation::{DocxExportResult, HtmlImportResult, MarkdownImportResult, Operation};
24use crate::{BlockFormat, BlockInfo, DocumentStats, FindMatch, FindOptions};
25
26/// A rich text document.
27///
28/// Owns the backend (database, event hub, undo/redo manager) and provides
29/// document-level operations. All cursor-based editing goes through
30/// [`TextCursor`], obtained via [`cursor()`](TextDocument::cursor) or
31/// [`cursor_at()`](TextDocument::cursor_at).
32///
33/// Internally uses `Arc<Mutex<...>>` so that multiple [`TextCursor`]s can
34/// coexist and edit concurrently. Cloning a `TextDocument` creates a new
35/// handle to the **same** underlying document (like Qt's implicit sharing).
36#[derive(Clone)]
37pub struct TextDocument {
38    pub(crate) inner: Arc<Mutex<TextDocumentInner>>,
39}
40
41impl TextDocument {
42    // ── Construction ──────────────────────────────────────────
43
44    /// Create a new, empty document.
45    ///
46    /// # Panics
47    ///
48    /// Panics if the database context cannot be created (e.g. filesystem error).
49    /// Use [`TextDocument::try_new`] for a fallible alternative.
50    pub fn new() -> Self {
51        Self::try_new().expect("failed to initialize document")
52    }
53
54    /// Create a new, empty document, returning an error on failure.
55    pub fn try_new() -> Result<Self> {
56        let ctx = frontend::AppContext::new();
57        let doc_inner = TextDocumentInner::initialize(ctx)?;
58        let inner = Arc::new(Mutex::new(doc_inner));
59
60        // Bridge backend long-operation events to public DocumentEvent.
61        Self::subscribe_long_operation_events(&inner);
62
63        Ok(Self { inner })
64    }
65
66    /// Subscribe to backend long-operation events and bridge them to DocumentEvent.
67    fn subscribe_long_operation_events(inner: &Arc<Mutex<TextDocumentInner>>) {
68        use frontend::common::event::{LongOperationEvent as LOE, Origin};
69
70        let weak = Arc::downgrade(inner);
71        {
72            let locked = inner.lock();
73            // Progress
74            let w = weak.clone();
75            locked
76                .event_client
77                .subscribe(Origin::LongOperation(LOE::Progress), move |event| {
78                    if let Some(inner) = w.upgrade() {
79                        let (op_id, percent, message) = parse_progress_data(&event.data);
80                        let mut inner = inner.lock();
81                        inner.queue_event(DocumentEvent::LongOperationProgress {
82                            operation_id: op_id,
83                            percent,
84                            message,
85                        });
86                    }
87                });
88
89            // Completed
90            let w = weak.clone();
91            locked
92                .event_client
93                .subscribe(Origin::LongOperation(LOE::Completed), move |event| {
94                    if let Some(inner) = w.upgrade() {
95                        let op_id = parse_id_data(&event.data);
96                        let mut inner = inner.lock();
97                        inner.queue_event(DocumentEvent::DocumentReset);
98                        inner.check_block_count_changed();
99                        inner.reset_cached_child_order();
100                        inner.queue_event(DocumentEvent::LongOperationFinished {
101                            operation_id: op_id,
102                            success: true,
103                            error: None,
104                        });
105                    }
106                });
107
108            // Cancelled
109            let w = weak.clone();
110            locked
111                .event_client
112                .subscribe(Origin::LongOperation(LOE::Cancelled), move |event| {
113                    if let Some(inner) = w.upgrade() {
114                        let op_id = parse_id_data(&event.data);
115                        let mut inner = inner.lock();
116                        inner.queue_event(DocumentEvent::LongOperationFinished {
117                            operation_id: op_id,
118                            success: false,
119                            error: Some("cancelled".into()),
120                        });
121                    }
122                });
123
124            // Failed
125            locked
126                .event_client
127                .subscribe(Origin::LongOperation(LOE::Failed), move |event| {
128                    if let Some(inner) = weak.upgrade() {
129                        let (op_id, error) = parse_failed_data(&event.data);
130                        let mut inner = inner.lock();
131                        inner.queue_event(DocumentEvent::LongOperationFinished {
132                            operation_id: op_id,
133                            success: false,
134                            error: Some(error),
135                        });
136                    }
137                });
138        }
139    }
140
141    // ── Whole-document content ────────────────────────────────
142
143    /// Replace the entire document with plain text. Clears undo history.
144    pub fn set_plain_text(&self, text: &str) -> Result<()> {
145        let queued = {
146            let mut inner = self.inner.lock();
147            let dto = frontend::document_io::ImportPlainTextDto {
148                plain_text: text.into(),
149            };
150            document_io_commands::import_plain_text(&inner.ctx, &dto)?;
151            undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
152            inner.invalidate_text_cache();
153            inner.rehighlight_all();
154            inner.queue_event(DocumentEvent::DocumentReset);
155            inner.check_block_count_changed();
156            inner.reset_cached_child_order();
157            inner.queue_event(DocumentEvent::UndoRedoChanged {
158                can_undo: false,
159                can_redo: false,
160            });
161            inner.take_queued_events()
162        };
163        crate::inner::dispatch_queued_events(queued);
164        Ok(())
165    }
166
167    /// Export the entire document as plain text.
168    pub fn to_plain_text(&self) -> Result<String> {
169        let mut inner = self.inner.lock();
170        Ok(inner.plain_text()?.to_string())
171    }
172
173    /// Replace the entire document with Markdown. Clears undo history.
174    ///
175    /// This is a **long operation**. Returns a typed [`Operation`] handle.
176    pub fn set_markdown(&self, markdown: &str) -> Result<Operation<MarkdownImportResult>> {
177        let mut inner = self.inner.lock();
178        inner.invalidate_text_cache();
179        let dto = frontend::document_io::ImportMarkdownDto {
180            markdown_text: markdown.into(),
181        };
182        let op_id = document_io_commands::import_markdown(&inner.ctx, &dto)?;
183        Ok(Operation::new(
184            op_id,
185            &inner.ctx,
186            Box::new(|ctx, id| {
187                document_io_commands::get_import_markdown_result(ctx, id)
188                    .ok()
189                    .flatten()
190                    .map(|r| {
191                        Ok(MarkdownImportResult {
192                            block_count: to_usize(r.block_count),
193                        })
194                    })
195            }),
196        ))
197    }
198
199    /// Export the entire document as Markdown.
200    pub fn to_markdown(&self) -> Result<String> {
201        let inner = self.inner.lock();
202        let dto = document_io_commands::export_markdown(&inner.ctx)?;
203        Ok(dto.markdown_text)
204    }
205
206    /// Replace the entire document with HTML. Clears undo history.
207    ///
208    /// This is a **long operation**. Returns a typed [`Operation`] handle.
209    pub fn set_html(&self, html: &str) -> Result<Operation<HtmlImportResult>> {
210        let mut inner = self.inner.lock();
211        inner.invalidate_text_cache();
212        let dto = frontend::document_io::ImportHtmlDto {
213            html_text: html.into(),
214        };
215        let op_id = document_io_commands::import_html(&inner.ctx, &dto)?;
216        Ok(Operation::new(
217            op_id,
218            &inner.ctx,
219            Box::new(|ctx, id| {
220                document_io_commands::get_import_html_result(ctx, id)
221                    .ok()
222                    .flatten()
223                    .map(|r| {
224                        Ok(HtmlImportResult {
225                            block_count: to_usize(r.block_count),
226                        })
227                    })
228            }),
229        ))
230    }
231
232    /// Export the entire document as HTML.
233    pub fn to_html(&self) -> Result<String> {
234        let inner = self.inner.lock();
235        let dto = document_io_commands::export_html(&inner.ctx)?;
236        Ok(dto.html_text)
237    }
238
239    /// Export the entire document as LaTeX.
240    pub fn to_latex(&self, document_class: &str, include_preamble: bool) -> Result<String> {
241        let inner = self.inner.lock();
242        let dto = frontend::document_io::ExportLatexDto {
243            document_class: document_class.into(),
244            include_preamble,
245        };
246        let result = document_io_commands::export_latex(&inner.ctx, &dto)?;
247        Ok(result.latex_text)
248    }
249
250    /// Export the entire document as DOCX to a file path.
251    ///
252    /// This is a **long operation**. Returns a typed [`Operation`] handle.
253    pub fn to_docx(&self, output_path: &str) -> Result<Operation<DocxExportResult>> {
254        let inner = self.inner.lock();
255        let dto = frontend::document_io::ExportDocxDto {
256            output_path: output_path.into(),
257        };
258        let op_id = document_io_commands::export_docx(&inner.ctx, &dto)?;
259        Ok(Operation::new(
260            op_id,
261            &inner.ctx,
262            Box::new(|ctx, id| {
263                document_io_commands::get_export_docx_result(ctx, id)
264                    .ok()
265                    .flatten()
266                    .map(|r| {
267                        Ok(DocxExportResult {
268                            file_path: r.file_path,
269                            paragraph_count: to_usize(r.paragraph_count),
270                        })
271                    })
272            }),
273        ))
274    }
275
276    /// Clear all document content and reset to an empty state.
277    pub fn clear(&self) -> Result<()> {
278        let queued = {
279            let mut inner = self.inner.lock();
280            let dto = frontend::document_io::ImportPlainTextDto {
281                plain_text: String::new(),
282            };
283            document_io_commands::import_plain_text(&inner.ctx, &dto)?;
284            undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
285            inner.invalidate_text_cache();
286            inner.rehighlight_all();
287            inner.queue_event(DocumentEvent::DocumentReset);
288            inner.check_block_count_changed();
289            inner.reset_cached_child_order();
290            inner.queue_event(DocumentEvent::UndoRedoChanged {
291                can_undo: false,
292                can_redo: false,
293            });
294            inner.take_queued_events()
295        };
296        crate::inner::dispatch_queued_events(queued);
297        Ok(())
298    }
299
300    // ── Cursor factory ───────────────────────────────────────
301
302    /// Create a cursor at position 0.
303    pub fn cursor(&self) -> TextCursor {
304        self.cursor_at(0)
305    }
306
307    /// Create a cursor at the given position.
308    pub fn cursor_at(&self, position: usize) -> TextCursor {
309        let data = {
310            let mut inner = self.inner.lock();
311            inner.register_cursor(position)
312        };
313        TextCursor {
314            doc: self.inner.clone(),
315            data,
316        }
317    }
318
319    // ── Document queries ─────────────────────────────────────
320
321    /// Get document statistics. O(1) — reads cached values.
322    pub fn stats(&self) -> DocumentStats {
323        let inner = self.inner.lock();
324        let dto = document_inspection_commands::get_document_stats(&inner.ctx)
325            .expect("get_document_stats should not fail");
326        DocumentStats::from(&dto)
327    }
328
329    /// Get the total character count. O(1) — reads cached value.
330    pub fn character_count(&self) -> usize {
331        let inner = self.inner.lock();
332        let dto = document_inspection_commands::get_document_stats(&inner.ctx)
333            .expect("get_document_stats should not fail");
334        to_usize(dto.character_count)
335    }
336
337    /// Get the number of blocks (paragraphs). O(1) — reads cached value.
338    pub fn block_count(&self) -> usize {
339        let inner = self.inner.lock();
340        let dto = document_inspection_commands::get_document_stats(&inner.ctx)
341            .expect("get_document_stats should not fail");
342        to_usize(dto.block_count)
343    }
344
345    /// Returns true if the document has no text content.
346    pub fn is_empty(&self) -> bool {
347        self.character_count() == 0
348    }
349
350    /// Get text at a position for a given length.
351    pub fn text_at(&self, position: usize, length: usize) -> Result<String> {
352        let inner = self.inner.lock();
353        let dto = frontend::document_inspection::GetTextAtPositionDto {
354            position: to_i64(position),
355            length: to_i64(length),
356        };
357        let result = document_inspection_commands::get_text_at_position(&inner.ctx, &dto)?;
358        Ok(result.text)
359    }
360
361    /// Find the inline element containing `position` and return its
362    /// stable entity id together with the element's absolute start
363    /// position and the character offset of `position` within the
364    /// element. Used by accessibility layers to convert a
365    /// document-absolute character position into the
366    /// `(element_id, character_index_in_run)` coordinate space
367    /// AccessKit's `TextPosition` expects.
368    ///
369    /// Returns `None` when the position is outside the document.
370    /// Returns the element at position `position - 1` when `position`
371    /// falls exactly on an element boundary, matching the "cursor
372    /// belongs to the preceding element at a boundary" convention
373    /// used throughout text-document.
374    pub fn find_element_at_position(&self, position: usize) -> Option<(u64, usize, usize)> {
375        let block_info = self.block_at(position).ok()?;
376        let block_start = block_info.start;
377        let offset_in_block = position.checked_sub(block_start)?;
378        let block = crate::text_block::TextBlock {
379            doc: std::sync::Arc::clone(&self.inner),
380            block_id: block_info.block_id,
381        };
382        let frags = block.fragments();
383        // Walk fragments; match the fragment that contains
384        // `offset_in_block`. For a boundary position shared with the
385        // next fragment, prefer the preceding fragment (boundary
386        // belongs to the end of the previous element).
387        let mut last_text: Option<(u64, usize, usize, usize)> = None; // (id, abs_start, frag_offset, frag_length)
388        for frag in &frags {
389            match frag {
390                crate::flow::FragmentContent::Text {
391                    offset,
392                    length,
393                    element_id,
394                    ..
395                } => {
396                    let frag_start = *offset;
397                    let frag_end = frag_start + *length;
398                    if offset_in_block >= frag_start && offset_in_block < frag_end {
399                        let abs_start = block_start + frag_start;
400                        let offset_within = offset_in_block - frag_start;
401                        return Some((*element_id, abs_start, offset_within));
402                    }
403                    // Record as a candidate for the "end-of-element"
404                    // boundary fallback (offset_in_block == frag_end).
405                    if offset_in_block == frag_end {
406                        last_text =
407                            Some((*element_id, block_start + frag_start, frag_start, *length));
408                    }
409                }
410                crate::flow::FragmentContent::Image {
411                    offset, element_id, ..
412                } => {
413                    if offset_in_block == *offset {
414                        return Some((*element_id, block_start + offset, 0));
415                    }
416                }
417            }
418        }
419        // Boundary fallback: position was at the end of the last text
420        // fragment we saw.
421        last_text.map(|(id, abs_start, _, length)| (id, abs_start, length))
422    }
423
424    /// Get info about the block at a position. O(log n).
425    pub fn block_at(&self, position: usize) -> Result<BlockInfo> {
426        let inner = self.inner.lock();
427        let dto = frontend::document_inspection::GetBlockAtPositionDto {
428            position: to_i64(position),
429        };
430        let result = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)?;
431        Ok(BlockInfo::from(&result))
432    }
433
434    /// Get the block format at a position.
435    pub fn block_format_at(&self, position: usize) -> Result<BlockFormat> {
436        let inner = self.inner.lock();
437        let dto = frontend::document_inspection::GetBlockAtPositionDto {
438            position: to_i64(position),
439        };
440        let block_info = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)?;
441        let block_id = block_info.block_id;
442        let block_id = block_id as u64;
443        let block_dto = frontend::commands::block_commands::get_block(&inner.ctx, &block_id)?
444            .ok_or_else(|| anyhow::anyhow!("block not found"))?;
445        Ok(BlockFormat::from(&block_dto))
446    }
447
448    // ── Flow traversal (layout engine API) ─────────────────
449
450    /// Walk the main frame's visual flow in document order.
451    ///
452    /// Returns the top-level flow elements — blocks, tables, and
453    /// sub-frames — in the order defined by the main frame's
454    /// `child_order`. Table cell contents are NOT included here;
455    /// access them through [`TextTableCell::blocks()`](crate::TextTableCell::blocks).
456    ///
457    /// This is the primary entry point for layout initialization.
458    pub fn flow(&self) -> Vec<crate::flow::FlowElement> {
459        let inner = self.inner.lock();
460        let main_frame_id = get_main_frame_id(&inner);
461        crate::text_frame::build_flow_elements(&inner, &self.inner, main_frame_id)
462    }
463
464    /// Get a read-only handle to a block by its entity ID.
465    ///
466    /// Entity IDs are stable across insertions and deletions.
467    /// Returns `None` if no block with this ID exists.
468    pub fn block_by_id(&self, block_id: usize) -> Option<crate::text_block::TextBlock> {
469        let inner = self.inner.lock();
470        let exists = frontend::commands::block_commands::get_block(&inner.ctx, &(block_id as u64))
471            .ok()
472            .flatten()
473            .is_some();
474
475        if exists {
476            Some(crate::text_block::TextBlock {
477                doc: self.inner.clone(),
478                block_id,
479            })
480        } else {
481            None
482        }
483    }
484
485    /// Build a single `BlockSnapshot` for the block at the given position.
486    ///
487    /// This is O(k) where k = inline elements in that block, compared to
488    /// `snapshot_flow()` which is O(n) over the entire document.
489    /// Use for incremental layout updates after single-block edits.
490    pub fn snapshot_block_at_position(
491        &self,
492        position: usize,
493    ) -> Option<crate::flow::BlockSnapshot> {
494        let inner = self.inner.lock();
495        let main_frame_id = get_main_frame_id(&inner);
496
497        // Collect all block IDs in document order, traversing into nested frames
498        let ordered_block_ids = collect_frame_block_ids(&inner, main_frame_id)?;
499
500        // Walk blocks computing positions on the fly
501        let pos = position as i64;
502        let mut running_pos: i64 = 0;
503        for &block_id in &ordered_block_ids {
504            let block_dto = block_commands::get_block(&inner.ctx, &block_id)
505                .ok()
506                .flatten()?;
507            let block_end = running_pos + block_dto.text_length;
508            if pos >= running_pos && pos <= block_end {
509                return crate::text_block::build_block_snapshot_with_position(
510                    &inner,
511                    block_id,
512                    Some(running_pos as usize),
513                );
514            }
515            running_pos = block_end + 1;
516        }
517
518        // Fallback to last block
519        if let Some(&last_id) = ordered_block_ids.last() {
520            return crate::text_block::build_block_snapshot(&inner, last_id);
521        }
522        None
523    }
524
525    /// Get a read-only handle to the block containing the given
526    /// character position. Returns `None` if position is out of range.
527    pub fn block_at_position(&self, position: usize) -> Option<crate::text_block::TextBlock> {
528        let inner = self.inner.lock();
529        let dto = frontend::document_inspection::GetBlockAtPositionDto {
530            position: to_i64(position),
531        };
532        let result = document_inspection_commands::get_block_at_position(&inner.ctx, &dto).ok()?;
533        Some(crate::text_block::TextBlock {
534            doc: self.inner.clone(),
535            block_id: result.block_id as usize,
536        })
537    }
538
539    /// Get a read-only handle to a block by its 0-indexed global
540    /// block number.
541    ///
542    /// **O(n)**: requires scanning all blocks sorted by
543    /// `document_position` to find the nth one. Prefer
544    /// [`block_at_position()`](TextDocument::block_at_position) or
545    /// [`block_by_id()`](TextDocument::block_by_id) in
546    /// performance-sensitive paths.
547    pub fn block_by_number(&self, block_number: usize) -> Option<crate::text_block::TextBlock> {
548        let inner = self.inner.lock();
549        let all_blocks = frontend::commands::block_commands::get_all_block(&inner.ctx).ok()?;
550        let mut sorted: Vec<_> = all_blocks.into_iter().collect();
551        sorted.sort_by_key(|b| b.document_position);
552
553        sorted
554            .get(block_number)
555            .map(|b| crate::text_block::TextBlock {
556                doc: self.inner.clone(),
557                block_id: b.id as usize,
558            })
559    }
560
561    /// All blocks in the document, sorted by `document_position`. **O(n)**.
562    ///
563    /// Returns blocks from all frames, including those inside table cells.
564    /// This is the efficient way to iterate all blocks — avoids the O(n^2)
565    /// cost of calling `block_by_number(i)` in a loop.
566    pub fn blocks(&self) -> Vec<crate::text_block::TextBlock> {
567        let inner = self.inner.lock();
568        let all_blocks =
569            frontend::commands::block_commands::get_all_block(&inner.ctx).unwrap_or_default();
570        let mut sorted: Vec<_> = all_blocks.into_iter().collect();
571        sorted.sort_by_key(|b| b.document_position);
572        sorted
573            .iter()
574            .map(|b| crate::text_block::TextBlock {
575                doc: self.inner.clone(),
576                block_id: b.id as usize,
577            })
578            .collect()
579    }
580
581    /// All blocks whose character range intersects `[position, position + length)`.
582    ///
583    /// **O(n)**: scans all blocks once. Returns them sorted by `document_position`.
584    /// A block intersects if its range `[block.position, block.position + block.length)`
585    /// overlaps the query range. An empty query range (`length == 0`) returns the
586    /// block containing that position, if any.
587    pub fn blocks_in_range(
588        &self,
589        position: usize,
590        length: usize,
591    ) -> Vec<crate::text_block::TextBlock> {
592        let inner = self.inner.lock();
593        let all_blocks =
594            frontend::commands::block_commands::get_all_block(&inner.ctx).unwrap_or_default();
595        let mut sorted: Vec<_> = all_blocks.into_iter().collect();
596        sorted.sort_by_key(|b| b.document_position);
597
598        let range_start = position;
599        let range_end = position + length;
600
601        sorted
602            .iter()
603            .filter(|b| {
604                let block_start = b.document_position.max(0) as usize;
605                let block_end = block_start + b.text_length.max(0) as usize;
606                // Overlap check: block intersects [range_start, range_end)
607                if length == 0 {
608                    // Point query: block contains the position
609                    range_start >= block_start && range_start < block_end
610                } else {
611                    block_start < range_end && block_end > range_start
612                }
613            })
614            .map(|b| crate::text_block::TextBlock {
615                doc: self.inner.clone(),
616                block_id: b.id as usize,
617            })
618            .collect()
619    }
620
621    /// Snapshot the entire main flow in a single lock acquisition.
622    ///
623    /// Returns a [`FlowSnapshot`](crate::FlowSnapshot) containing snapshots
624    /// for every element in the flow.
625    pub fn snapshot_flow(&self) -> crate::flow::FlowSnapshot {
626        let inner = self.inner.lock();
627        let main_frame_id = get_main_frame_id(&inner);
628        let elements = crate::text_frame::build_flow_snapshot(&inner, main_frame_id);
629        crate::flow::FlowSnapshot { elements }
630    }
631
632    // ── Search ───────────────────────────────────────────────
633
634    /// Find the next (or previous) occurrence. Returns `None` if not found.
635    pub fn find(
636        &self,
637        query: &str,
638        from: usize,
639        options: &FindOptions,
640    ) -> Result<Option<FindMatch>> {
641        let inner = self.inner.lock();
642        let dto = options.to_find_text_dto(query, from);
643        let result = document_search_commands::find_text(&inner.ctx, &dto)?;
644        Ok(convert::find_result_to_match(&result))
645    }
646
647    /// Find all occurrences.
648    pub fn find_all(&self, query: &str, options: &FindOptions) -> Result<Vec<FindMatch>> {
649        let inner = self.inner.lock();
650        let dto = options.to_find_all_dto(query);
651        let result = document_search_commands::find_all(&inner.ctx, &dto)?;
652        Ok(convert::find_all_to_matches(&result))
653    }
654
655    /// Replace occurrences. Returns the number of replacements. Undoable.
656    pub fn replace_text(
657        &self,
658        query: &str,
659        replacement: &str,
660        replace_all: bool,
661        options: &FindOptions,
662    ) -> Result<usize> {
663        let (count, queued) = {
664            let mut inner = self.inner.lock();
665            let dto = options.to_replace_dto(query, replacement, replace_all);
666            let result =
667                document_search_commands::replace_text(&inner.ctx, Some(inner.stack_id), &dto)?;
668            let count = to_usize(result.replacements_count);
669            inner.invalidate_text_cache();
670            if count > 0 {
671                inner.modified = true;
672                inner.rehighlight_all();
673                // Replacements are scattered across the document — we can't
674                // provide a single position/chars delta. Signal "content changed
675                // from position 0, affecting `count` sites" so the consumer
676                // knows to re-read.
677                inner.queue_event(DocumentEvent::ContentsChanged {
678                    position: 0,
679                    chars_removed: 0,
680                    chars_added: 0,
681                    blocks_affected: count,
682                });
683                inner.check_block_count_changed();
684                inner.check_flow_changed();
685                let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
686                let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
687                inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
688            }
689            (count, inner.take_queued_events())
690        };
691        crate::inner::dispatch_queued_events(queued);
692        Ok(count)
693    }
694
695    // ── Resources ────────────────────────────────────────────
696
697    /// Add a resource (image, stylesheet) to the document.
698    pub fn add_resource(
699        &self,
700        resource_type: ResourceType,
701        name: &str,
702        mime_type: &str,
703        data: &[u8],
704    ) -> Result<()> {
705        let mut inner = self.inner.lock();
706        let dto = frontend::resource::dtos::CreateResourceDto {
707            created_at: Default::default(),
708            updated_at: Default::default(),
709            resource_type,
710            name: name.into(),
711            url: String::new(),
712            mime_type: mime_type.into(),
713            data_base64: BASE64.encode(data),
714        };
715        let created = resource_commands::create_resource(
716            &inner.ctx,
717            Some(inner.stack_id),
718            &dto,
719            inner.document_id,
720            -1,
721        )?;
722        inner.resource_cache.insert(name.to_string(), created.id);
723        Ok(())
724    }
725
726    /// Get a resource by name. Returns `None` if not found.
727    ///
728    /// Uses an internal cache to avoid scanning all resources on repeated lookups.
729    pub fn resource(&self, name: &str) -> Result<Option<Vec<u8>>> {
730        let mut inner = self.inner.lock();
731
732        // Fast path: check the name → ID cache.
733        if let Some(&id) = inner.resource_cache.get(name) {
734            if let Some(r) = resource_commands::get_resource(&inner.ctx, &id)? {
735                let bytes = BASE64.decode(&r.data_base64)?;
736                return Ok(Some(bytes));
737            }
738            // ID was stale — fall through to full scan.
739            inner.resource_cache.remove(name);
740        }
741
742        // Slow path: linear scan, then populate cache for the match.
743        let all = resource_commands::get_all_resource(&inner.ctx)?;
744        for r in &all {
745            if r.name == name {
746                inner.resource_cache.insert(name.to_string(), r.id);
747                let bytes = BASE64.decode(&r.data_base64)?;
748                return Ok(Some(bytes));
749            }
750        }
751        Ok(None)
752    }
753
754    // ── Undo / Redo ──────────────────────────────────────────
755
756    /// Undo the last operation.
757    pub fn undo(&self) -> Result<()> {
758        let queued = {
759            let mut inner = self.inner.lock();
760            let before = capture_block_state(&inner);
761            let result = undo_redo_commands::undo(&inner.ctx, Some(inner.stack_id));
762            inner.invalidate_text_cache();
763            result?;
764            inner.rehighlight_all();
765            emit_undo_redo_change_events(&mut inner, &before);
766            inner.check_block_count_changed();
767            inner.check_flow_changed();
768            let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
769            let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
770            inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
771            inner.take_queued_events()
772        };
773        crate::inner::dispatch_queued_events(queued);
774        Ok(())
775    }
776
777    /// Redo the last undone operation.
778    pub fn redo(&self) -> Result<()> {
779        let queued = {
780            let mut inner = self.inner.lock();
781            let before = capture_block_state(&inner);
782            let result = undo_redo_commands::redo(&inner.ctx, Some(inner.stack_id));
783            inner.invalidate_text_cache();
784            result?;
785            inner.rehighlight_all();
786            emit_undo_redo_change_events(&mut inner, &before);
787            inner.check_block_count_changed();
788            inner.check_flow_changed();
789            let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
790            let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
791            inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
792            inner.take_queued_events()
793        };
794        crate::inner::dispatch_queued_events(queued);
795        Ok(())
796    }
797
798    /// Returns true if there are operations that can be undone.
799    pub fn can_undo(&self) -> bool {
800        let inner = self.inner.lock();
801        undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id))
802    }
803
804    /// Returns true if there are operations that can be redone.
805    pub fn can_redo(&self) -> bool {
806        let inner = self.inner.lock();
807        undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id))
808    }
809
810    /// Clear all undo/redo history.
811    pub fn clear_undo_redo(&self) {
812        let inner = self.inner.lock();
813        undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
814    }
815
816    // ── Modified state ───────────────────────────────────────
817
818    /// Returns true if the document has been modified since creation or last reset.
819    pub fn is_modified(&self) -> bool {
820        self.inner.lock().modified
821    }
822
823    /// Set or clear the modified flag.
824    pub fn set_modified(&self, modified: bool) {
825        let queued = {
826            let mut inner = self.inner.lock();
827            if inner.modified != modified {
828                inner.modified = modified;
829                inner.queue_event(DocumentEvent::ModificationChanged(modified));
830            }
831            inner.take_queued_events()
832        };
833        crate::inner::dispatch_queued_events(queued);
834    }
835
836    // ── Document properties ──────────────────────────────────
837
838    /// Get the document title.
839    pub fn title(&self) -> String {
840        let inner = self.inner.lock();
841        document_commands::get_document(&inner.ctx, &inner.document_id)
842            .ok()
843            .flatten()
844            .map(|d| d.title)
845            .unwrap_or_default()
846    }
847
848    /// Set the document title.
849    pub fn set_title(&self, title: &str) -> Result<()> {
850        let inner = self.inner.lock();
851        let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
852            .ok_or_else(|| anyhow::anyhow!("document not found"))?;
853        let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
854        update.title = title.into();
855        document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
856        Ok(())
857    }
858
859    /// Get the text direction.
860    pub fn text_direction(&self) -> TextDirection {
861        let inner = self.inner.lock();
862        document_commands::get_document(&inner.ctx, &inner.document_id)
863            .ok()
864            .flatten()
865            .map(|d| d.text_direction)
866            .unwrap_or(TextDirection::LeftToRight)
867    }
868
869    /// Set the text direction.
870    pub fn set_text_direction(&self, direction: TextDirection) -> Result<()> {
871        let inner = self.inner.lock();
872        let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
873            .ok_or_else(|| anyhow::anyhow!("document not found"))?;
874        let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
875        update.text_direction = direction;
876        document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
877        Ok(())
878    }
879
880    /// Get the default wrap mode.
881    pub fn default_wrap_mode(&self) -> WrapMode {
882        let inner = self.inner.lock();
883        document_commands::get_document(&inner.ctx, &inner.document_id)
884            .ok()
885            .flatten()
886            .map(|d| d.default_wrap_mode)
887            .unwrap_or(WrapMode::WordWrap)
888    }
889
890    /// Set the default wrap mode.
891    pub fn set_default_wrap_mode(&self, mode: WrapMode) -> Result<()> {
892        let inner = self.inner.lock();
893        let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
894            .ok_or_else(|| anyhow::anyhow!("document not found"))?;
895        let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
896        update.default_wrap_mode = mode;
897        document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
898        Ok(())
899    }
900
901    // ── Event subscription ───────────────────────────────────
902
903    /// Subscribe to document events via callback.
904    ///
905    /// Callbacks are invoked **outside** the document lock (after the editing
906    /// operation completes and the lock is released). It is safe to call
907    /// `TextDocument` or `TextCursor` methods from within the callback without
908    /// risk of deadlock. However, keep callbacks lightweight — they run
909    /// synchronously on the calling thread and block the caller until they
910    /// return.
911    ///
912    /// Drop the returned [`Subscription`] to unsubscribe.
913    ///
914    /// # Breaking change (v0.0.6)
915    ///
916    /// The callback bound changed from `Send` to `Send + Sync` in v0.0.6
917    /// to support `Arc`-based dispatch. Callbacks that capture non-`Sync`
918    /// types (e.g., `Rc<T>`, `Cell<T>`) must be wrapped in a `Mutex`.
919    pub fn on_change<F>(&self, callback: F) -> Subscription
920    where
921        F: Fn(DocumentEvent) + Send + Sync + 'static,
922    {
923        let mut inner = self.inner.lock();
924        events::subscribe_inner(&mut inner, callback)
925    }
926
927    /// Return events accumulated since the last `poll_events()` call.
928    ///
929    /// This delivery path is independent of callback dispatch via
930    /// [`on_change`](Self::on_change) — using both simultaneously is safe
931    /// and each path sees every event exactly once.
932    pub fn poll_events(&self) -> Vec<DocumentEvent> {
933        let mut inner = self.inner.lock();
934        inner.drain_poll_events()
935    }
936
937    // ── Syntax highlighting ──────────────────────────────────
938
939    /// Attach a syntax highlighter to this document.
940    ///
941    /// Immediately re-highlights the entire document. Replaces any
942    /// previously attached highlighter. Pass `None` to remove the
943    /// highlighter and clear all highlight formatting.
944    pub fn set_syntax_highlighter(&self, highlighter: Option<Arc<dyn crate::SyntaxHighlighter>>) {
945        let mut inner = self.inner.lock();
946        match highlighter {
947            Some(hl) => {
948                inner.highlight = Some(crate::highlight::HighlightData {
949                    highlighter: hl,
950                    blocks: std::collections::HashMap::new(),
951                });
952                inner.rehighlight_all();
953            }
954            None => {
955                inner.highlight = None;
956            }
957        }
958    }
959
960    /// Re-highlight the entire document.
961    ///
962    /// Call this when the highlighter's rules change (e.g., new keywords
963    /// were added, spellcheck dictionary updated).
964    pub fn rehighlight(&self) {
965        let mut inner = self.inner.lock();
966        inner.rehighlight_all();
967    }
968
969    /// Re-highlight a single block and cascade to subsequent blocks if
970    /// the block state changes.
971    pub fn rehighlight_block(&self, block_id: usize) {
972        let mut inner = self.inner.lock();
973        inner.rehighlight_from_block(block_id);
974    }
975}
976
977impl Default for TextDocument {
978    fn default() -> Self {
979        Self::new()
980    }
981}
982
983// ── Undo/redo change detection helpers ─────────────────────────
984
985/// Lightweight block state for before/after comparison during undo/redo.
986struct UndoBlockState {
987    id: u64,
988    position: i64,
989    text_length: i64,
990    plain_text: String,
991    format: BlockFormat,
992}
993
994/// Capture the state of all blocks, sorted by document_position.
995fn capture_block_state(inner: &TextDocumentInner) -> Vec<UndoBlockState> {
996    let all_blocks =
997        frontend::commands::block_commands::get_all_block(&inner.ctx).unwrap_or_default();
998    let mut states: Vec<UndoBlockState> = all_blocks
999        .into_iter()
1000        .map(|b| UndoBlockState {
1001            id: b.id,
1002            position: b.document_position,
1003            text_length: b.text_length,
1004            plain_text: b.plain_text.clone(),
1005            format: BlockFormat::from(&b),
1006        })
1007        .collect();
1008    states.sort_by_key(|s| s.position);
1009    states
1010}
1011
1012/// Build the full document text from sorted block states (joined with newlines).
1013fn build_doc_text(states: &[UndoBlockState]) -> String {
1014    states
1015        .iter()
1016        .map(|s| s.plain_text.as_str())
1017        .collect::<Vec<_>>()
1018        .join("\n")
1019}
1020
1021/// Compute the precise edit between two strings by comparing common prefix and suffix.
1022/// Returns `(edit_offset, chars_removed, chars_added)`.
1023fn compute_text_edit(before: &str, after: &str) -> (usize, usize, usize) {
1024    let before_chars: Vec<char> = before.chars().collect();
1025    let after_chars: Vec<char> = after.chars().collect();
1026
1027    // Common prefix
1028    let prefix_len = before_chars
1029        .iter()
1030        .zip(after_chars.iter())
1031        .take_while(|(a, b)| a == b)
1032        .count();
1033
1034    // Common suffix (not overlapping with prefix)
1035    let before_remaining = before_chars.len() - prefix_len;
1036    let after_remaining = after_chars.len() - prefix_len;
1037    let suffix_len = before_chars
1038        .iter()
1039        .rev()
1040        .zip(after_chars.iter().rev())
1041        .take(before_remaining.min(after_remaining))
1042        .take_while(|(a, b)| a == b)
1043        .count();
1044
1045    let removed = before_remaining - suffix_len;
1046    let added = after_remaining - suffix_len;
1047
1048    (prefix_len, removed, added)
1049}
1050
1051/// Compare block state before and after undo/redo and emit
1052/// ContentsChanged / FormatChanged events for affected regions.
1053fn emit_undo_redo_change_events(inner: &mut TextDocumentInner, before: &[UndoBlockState]) {
1054    let after = capture_block_state(inner);
1055
1056    // Build a map of block id → state for the "before" set.
1057    let before_map: std::collections::HashMap<u64, &UndoBlockState> =
1058        before.iter().map(|s| (s.id, s)).collect();
1059    let after_map: std::collections::HashMap<u64, &UndoBlockState> =
1060        after.iter().map(|s| (s.id, s)).collect();
1061
1062    // Track the affected content region (earliest position, total old/new length).
1063    let mut content_changed = false;
1064    let mut earliest_pos: Option<usize> = None;
1065    let mut old_end: usize = 0;
1066    let mut new_end: usize = 0;
1067    let mut blocks_affected: usize = 0;
1068
1069    let mut format_only_changes: Vec<(usize, usize)> = Vec::new(); // (position, length)
1070
1071    // Check blocks present in both before and after.
1072    for after_state in &after {
1073        if let Some(before_state) = before_map.get(&after_state.id) {
1074            let text_changed = before_state.plain_text != after_state.plain_text
1075                || before_state.text_length != after_state.text_length;
1076            let format_changed = before_state.format != after_state.format;
1077
1078            if text_changed {
1079                content_changed = true;
1080                blocks_affected += 1;
1081                let pos = after_state.position.max(0) as usize;
1082                earliest_pos = Some(earliest_pos.map_or(pos, |p: usize| p.min(pos)));
1083                old_end = old_end.max(
1084                    before_state.position.max(0) as usize
1085                        + before_state.text_length.max(0) as usize,
1086                );
1087                new_end = new_end.max(pos + after_state.text_length.max(0) as usize);
1088            } else if format_changed {
1089                let pos = after_state.position.max(0) as usize;
1090                let len = after_state.text_length.max(0) as usize;
1091                format_only_changes.push((pos, len));
1092            }
1093        } else {
1094            // Block exists in after but not in before — new block from undo/redo.
1095            content_changed = true;
1096            blocks_affected += 1;
1097            let pos = after_state.position.max(0) as usize;
1098            earliest_pos = Some(earliest_pos.map_or(pos, |p: usize| p.min(pos)));
1099            new_end = new_end.max(pos + after_state.text_length.max(0) as usize);
1100        }
1101    }
1102
1103    // Check blocks that were removed (present in before but not after).
1104    for before_state in before {
1105        if !after_map.contains_key(&before_state.id) {
1106            content_changed = true;
1107            blocks_affected += 1;
1108            let pos = before_state.position.max(0) as usize;
1109            earliest_pos = Some(earliest_pos.map_or(pos, |p: usize| p.min(pos)));
1110            old_end = old_end.max(pos + before_state.text_length.max(0) as usize);
1111        }
1112    }
1113
1114    if content_changed {
1115        let position = earliest_pos.unwrap_or(0);
1116        let chars_removed = old_end.saturating_sub(position);
1117        let chars_added = new_end.saturating_sub(position);
1118
1119        // Use a precise text-level diff for cursor adjustment so cursors land
1120        // at the actual edit point rather than the end of the affected block.
1121        let before_text = build_doc_text(before);
1122        let after_text = build_doc_text(&after);
1123        let (edit_offset, precise_removed, precise_added) =
1124            compute_text_edit(&before_text, &after_text);
1125        if precise_removed > 0 || precise_added > 0 {
1126            inner.adjust_cursors(edit_offset, precise_removed, precise_added);
1127        }
1128
1129        inner.queue_event(DocumentEvent::ContentsChanged {
1130            position,
1131            chars_removed,
1132            chars_added,
1133            blocks_affected,
1134        });
1135    }
1136
1137    // Emit FormatChanged for blocks where only formatting changed (not content).
1138    for (position, length) in format_only_changes {
1139        inner.queue_event(DocumentEvent::FormatChanged {
1140            position,
1141            length,
1142            kind: FormatChangeKind::Block,
1143        });
1144    }
1145}
1146
1147// ── Flow helpers ──────────────────────────────────────────────
1148
1149/// Get the main frame ID for the document.
1150/// Collect all block IDs in document order from a frame, recursing into nested
1151/// sub-frames (negative entries in child_order).
1152fn collect_frame_block_ids(
1153    inner: &TextDocumentInner,
1154    frame_id: frontend::common::types::EntityId,
1155) -> Option<Vec<u64>> {
1156    let frame_dto = frame_commands::get_frame(&inner.ctx, &frame_id)
1157        .ok()
1158        .flatten()?;
1159
1160    if !frame_dto.child_order.is_empty() {
1161        let mut block_ids = Vec::new();
1162        for &entry in &frame_dto.child_order {
1163            if entry > 0 {
1164                block_ids.push(entry as u64);
1165            } else if entry < 0 {
1166                let sub_frame_id = (-entry) as u64;
1167                let sub_frame = frame_commands::get_frame(&inner.ctx, &sub_frame_id)
1168                    .ok()
1169                    .flatten();
1170                if let Some(ref sf) = sub_frame {
1171                    if let Some(table_id) = sf.table {
1172                        // Table anchor frame: collect blocks from cell frames
1173                        // in row-major order, matching collect_block_ids_recursive.
1174                        if let Some(table_dto) = table_commands::get_table(&inner.ctx, &table_id)
1175                            .ok()
1176                            .flatten()
1177                        {
1178                            let mut cell_dtos: Vec<_> = table_dto
1179                                .cells
1180                                .iter()
1181                                .filter_map(|&cid| {
1182                                    table_cell_commands::get_table_cell(&inner.ctx, &cid)
1183                                        .ok()
1184                                        .flatten()
1185                                })
1186                                .collect();
1187                            cell_dtos
1188                                .sort_by(|a, b| a.row.cmp(&b.row).then(a.column.cmp(&b.column)));
1189                            for cell_dto in &cell_dtos {
1190                                if let Some(cf_id) = cell_dto.cell_frame
1191                                    && let Some(cf_ids) = collect_frame_block_ids(inner, cf_id)
1192                                {
1193                                    block_ids.extend(cf_ids);
1194                                }
1195                            }
1196                        }
1197                    } else if let Some(sub_ids) = collect_frame_block_ids(inner, sub_frame_id) {
1198                        block_ids.extend(sub_ids);
1199                    }
1200                }
1201            }
1202        }
1203        Some(block_ids)
1204    } else {
1205        Some(frame_dto.blocks.to_vec())
1206    }
1207}
1208
1209pub(crate) fn get_main_frame_id(inner: &TextDocumentInner) -> frontend::common::types::EntityId {
1210    // The document's first frame is the main frame.
1211    let frames = frontend::commands::document_commands::get_document_relationship(
1212        &inner.ctx,
1213        &inner.document_id,
1214        &frontend::document::dtos::DocumentRelationshipField::Frames,
1215    )
1216    .unwrap_or_default();
1217
1218    frames.first().copied().unwrap_or(0)
1219}
1220
1221// ── Long-operation event data helpers ─────────────────────────
1222
1223/// Parse progress JSON: `{"id":"...", "percentage": 50.0, "message": "..."}`
1224fn parse_progress_data(data: &Option<String>) -> (String, f64, String) {
1225    let Some(json) = data else {
1226        return (String::new(), 0.0, String::new());
1227    };
1228    let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
1229    let id = v["id"].as_str().unwrap_or_default().to_string();
1230    let pct = v["percentage"].as_f64().unwrap_or(0.0);
1231    let msg = v["message"].as_str().unwrap_or_default().to_string();
1232    (id, pct, msg)
1233}
1234
1235/// Parse completed/cancelled JSON: `{"id":"..."}`
1236fn parse_id_data(data: &Option<String>) -> String {
1237    let Some(json) = data else {
1238        return String::new();
1239    };
1240    let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
1241    v["id"].as_str().unwrap_or_default().to_string()
1242}
1243
1244/// Parse failed JSON: `{"id":"...", "error":"..."}`
1245fn parse_failed_data(data: &Option<String>) -> (String, String) {
1246    let Some(json) = data else {
1247        return (String::new(), "unknown error".into());
1248    };
1249    let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
1250    let id = v["id"].as_str().unwrap_or_default().to_string();
1251    let error = v["error"].as_str().unwrap_or("unknown error").to_string();
1252    (id, error)
1253}