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