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