Skip to main content

text_document/
document.rs

1//! TextDocument implementation.
2
3use std::sync::Arc;
4
5use parking_lot::Mutex;
6
7use anyhow::Result;
8use base64::Engine;
9use base64::engine::general_purpose::STANDARD as BASE64;
10
11use crate::{ResourceType, TextDirection, WrapMode};
12use frontend::commands::{
13    document_commands, document_inspection_commands, document_io_commands,
14    document_search_commands, resource_commands, undo_redo_commands,
15};
16
17use crate::convert::{self, to_i64, to_usize};
18use crate::cursor::TextCursor;
19use crate::events::{self, DocumentEvent, Subscription};
20use crate::flow::FormatChangeKind;
21use crate::inner::TextDocumentInner;
22use crate::operation::{DocxExportResult, HtmlImportResult, MarkdownImportResult, Operation};
23use crate::{BlockFormat, BlockInfo, DocumentStats, FindMatch, FindOptions};
24
25/// A rich text document.
26///
27/// Owns the backend (database, event hub, undo/redo manager) and provides
28/// document-level operations. All cursor-based editing goes through
29/// [`TextCursor`], obtained via [`cursor()`](TextDocument::cursor) or
30/// [`cursor_at()`](TextDocument::cursor_at).
31///
32/// Internally uses `Arc<Mutex<...>>` so that multiple [`TextCursor`]s can
33/// coexist and edit concurrently. Cloning a `TextDocument` creates a new
34/// handle to the **same** underlying document (like Qt's implicit sharing).
35#[derive(Clone)]
36pub struct TextDocument {
37    pub(crate) inner: Arc<Mutex<TextDocumentInner>>,
38}
39
40impl TextDocument {
41    // ── Construction ──────────────────────────────────────────
42
43    /// Create a new, empty document.
44    ///
45    /// # Panics
46    ///
47    /// Panics if the database context cannot be created (e.g. filesystem error).
48    /// Use [`TextDocument::try_new`] for a fallible alternative.
49    pub fn new() -> Self {
50        Self::try_new().expect("failed to initialize document")
51    }
52
53    /// Create a new, empty document, returning an error on failure.
54    pub fn try_new() -> Result<Self> {
55        let ctx = frontend::AppContext::new();
56        let doc_inner = TextDocumentInner::initialize(ctx)?;
57        let inner = Arc::new(Mutex::new(doc_inner));
58
59        // Bridge backend long-operation events to public DocumentEvent.
60        Self::subscribe_long_operation_events(&inner);
61
62        Ok(Self { inner })
63    }
64
65    /// Subscribe to backend long-operation events and bridge them to DocumentEvent.
66    fn subscribe_long_operation_events(inner: &Arc<Mutex<TextDocumentInner>>) {
67        use frontend::common::event::{LongOperationEvent as LOE, Origin};
68
69        let weak = Arc::downgrade(inner);
70        {
71            let locked = inner.lock();
72            // Progress
73            let w = weak.clone();
74            locked
75                .event_client
76                .subscribe(Origin::LongOperation(LOE::Progress), move |event| {
77                    if let Some(inner) = w.upgrade() {
78                        let (op_id, percent, message) = parse_progress_data(&event.data);
79                        let mut inner = inner.lock();
80                        inner.queue_event(DocumentEvent::LongOperationProgress {
81                            operation_id: op_id,
82                            percent,
83                            message,
84                        });
85                    }
86                });
87
88            // Completed
89            let w = weak.clone();
90            locked
91                .event_client
92                .subscribe(Origin::LongOperation(LOE::Completed), move |event| {
93                    if let Some(inner) = w.upgrade() {
94                        let op_id = parse_id_data(&event.data);
95                        let mut inner = inner.lock();
96                        inner.queue_event(DocumentEvent::DocumentReset);
97                        inner.check_block_count_changed();
98                        inner.reset_cached_child_order();
99                        inner.queue_event(DocumentEvent::LongOperationFinished {
100                            operation_id: op_id,
101                            success: true,
102                            error: None,
103                        });
104                    }
105                });
106
107            // Cancelled
108            let w = weak.clone();
109            locked
110                .event_client
111                .subscribe(Origin::LongOperation(LOE::Cancelled), move |event| {
112                    if let Some(inner) = w.upgrade() {
113                        let op_id = parse_id_data(&event.data);
114                        let mut inner = inner.lock();
115                        inner.queue_event(DocumentEvent::LongOperationFinished {
116                            operation_id: op_id,
117                            success: false,
118                            error: Some("cancelled".into()),
119                        });
120                    }
121                });
122
123            // Failed
124            locked
125                .event_client
126                .subscribe(Origin::LongOperation(LOE::Failed), move |event| {
127                    if let Some(inner) = weak.upgrade() {
128                        let (op_id, error) = parse_failed_data(&event.data);
129                        let mut inner = inner.lock();
130                        inner.queue_event(DocumentEvent::LongOperationFinished {
131                            operation_id: op_id,
132                            success: false,
133                            error: Some(error),
134                        });
135                    }
136                });
137        }
138    }
139
140    // ── Whole-document content ────────────────────────────────
141
142    /// Replace the entire document with plain text. Clears undo history.
143    pub fn set_plain_text(&self, text: &str) -> Result<()> {
144        let queued = {
145            let mut inner = self.inner.lock();
146            let dto = frontend::document_io::ImportPlainTextDto {
147                plain_text: text.into(),
148            };
149            document_io_commands::import_plain_text(&inner.ctx, &dto)?;
150            undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
151            inner.invalidate_text_cache();
152            inner.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    /// Get a read-only handle to the block containing the given
422    /// character position. Returns `None` if position is out of range.
423    pub fn block_at_position(&self, position: usize) -> Option<crate::text_block::TextBlock> {
424        let inner = self.inner.lock();
425        let dto = frontend::document_inspection::GetBlockAtPositionDto {
426            position: to_i64(position),
427        };
428        let result = document_inspection_commands::get_block_at_position(&inner.ctx, &dto).ok()?;
429        Some(crate::text_block::TextBlock {
430            doc: self.inner.clone(),
431            block_id: result.block_id as usize,
432        })
433    }
434
435    /// Get a read-only handle to a block by its 0-indexed global
436    /// block number.
437    ///
438    /// **O(n)**: requires scanning all blocks sorted by
439    /// `document_position` to find the nth one. Prefer
440    /// [`block_at_position()`](TextDocument::block_at_position) or
441    /// [`block_by_id()`](TextDocument::block_by_id) in
442    /// performance-sensitive paths.
443    pub fn block_by_number(&self, block_number: usize) -> Option<crate::text_block::TextBlock> {
444        let inner = self.inner.lock();
445        let all_blocks = frontend::commands::block_commands::get_all_block(&inner.ctx).ok()?;
446        let mut sorted: Vec<_> = all_blocks.into_iter().collect();
447        sorted.sort_by_key(|b| b.document_position);
448
449        sorted
450            .get(block_number)
451            .map(|b| crate::text_block::TextBlock {
452                doc: self.inner.clone(),
453                block_id: b.id as usize,
454            })
455    }
456
457    /// All blocks in the document, sorted by `document_position`. **O(n)**.
458    ///
459    /// Returns blocks from all frames, including those inside table cells.
460    /// This is the efficient way to iterate all blocks — avoids the O(n^2)
461    /// cost of calling `block_by_number(i)` in a loop.
462    pub fn blocks(&self) -> Vec<crate::text_block::TextBlock> {
463        let inner = self.inner.lock();
464        let all_blocks =
465            frontend::commands::block_commands::get_all_block(&inner.ctx).unwrap_or_default();
466        let mut sorted: Vec<_> = all_blocks.into_iter().collect();
467        sorted.sort_by_key(|b| b.document_position);
468        sorted
469            .iter()
470            .map(|b| crate::text_block::TextBlock {
471                doc: self.inner.clone(),
472                block_id: b.id as usize,
473            })
474            .collect()
475    }
476
477    /// All blocks whose character range intersects `[position, position + length)`.
478    ///
479    /// **O(n)**: scans all blocks once. Returns them sorted by `document_position`.
480    /// A block intersects if its range `[block.position, block.position + block.length)`
481    /// overlaps the query range. An empty query range (`length == 0`) returns the
482    /// block containing that position, if any.
483    pub fn blocks_in_range(
484        &self,
485        position: usize,
486        length: usize,
487    ) -> Vec<crate::text_block::TextBlock> {
488        let inner = self.inner.lock();
489        let all_blocks =
490            frontend::commands::block_commands::get_all_block(&inner.ctx).unwrap_or_default();
491        let mut sorted: Vec<_> = all_blocks.into_iter().collect();
492        sorted.sort_by_key(|b| b.document_position);
493
494        let range_start = position;
495        let range_end = position + length;
496
497        sorted
498            .iter()
499            .filter(|b| {
500                let block_start = b.document_position.max(0) as usize;
501                let block_end = block_start + b.text_length.max(0) as usize;
502                // Overlap check: block intersects [range_start, range_end)
503                if length == 0 {
504                    // Point query: block contains the position
505                    range_start >= block_start && range_start < block_end
506                } else {
507                    block_start < range_end && block_end > range_start
508                }
509            })
510            .map(|b| crate::text_block::TextBlock {
511                doc: self.inner.clone(),
512                block_id: b.id as usize,
513            })
514            .collect()
515    }
516
517    /// Snapshot the entire main flow in a single lock acquisition.
518    ///
519    /// Returns a [`FlowSnapshot`](crate::FlowSnapshot) containing snapshots
520    /// for every element in the flow.
521    pub fn snapshot_flow(&self) -> crate::flow::FlowSnapshot {
522        let inner = self.inner.lock();
523        let main_frame_id = get_main_frame_id(&inner);
524        let elements = crate::text_frame::build_flow_snapshot(&inner, main_frame_id);
525        crate::flow::FlowSnapshot { elements }
526    }
527
528    // ── Search ───────────────────────────────────────────────
529
530    /// Find the next (or previous) occurrence. Returns `None` if not found.
531    pub fn find(
532        &self,
533        query: &str,
534        from: usize,
535        options: &FindOptions,
536    ) -> Result<Option<FindMatch>> {
537        let inner = self.inner.lock();
538        let dto = options.to_find_text_dto(query, from);
539        let result = document_search_commands::find_text(&inner.ctx, &dto)?;
540        Ok(convert::find_result_to_match(&result))
541    }
542
543    /// Find all occurrences.
544    pub fn find_all(&self, query: &str, options: &FindOptions) -> Result<Vec<FindMatch>> {
545        let inner = self.inner.lock();
546        let dto = options.to_find_all_dto(query);
547        let result = document_search_commands::find_all(&inner.ctx, &dto)?;
548        Ok(convert::find_all_to_matches(&result))
549    }
550
551    /// Replace occurrences. Returns the number of replacements. Undoable.
552    pub fn replace_text(
553        &self,
554        query: &str,
555        replacement: &str,
556        replace_all: bool,
557        options: &FindOptions,
558    ) -> Result<usize> {
559        let (count, queued) = {
560            let mut inner = self.inner.lock();
561            let dto = options.to_replace_dto(query, replacement, replace_all);
562            let result =
563                document_search_commands::replace_text(&inner.ctx, Some(inner.stack_id), &dto)?;
564            let count = to_usize(result.replacements_count);
565            inner.invalidate_text_cache();
566            if count > 0 {
567                inner.modified = true;
568                inner.rehighlight_all();
569                // Replacements are scattered across the document — we can't
570                // provide a single position/chars delta. Signal "content changed
571                // from position 0, affecting `count` sites" so the consumer
572                // knows to re-read.
573                inner.queue_event(DocumentEvent::ContentsChanged {
574                    position: 0,
575                    chars_removed: 0,
576                    chars_added: 0,
577                    blocks_affected: count,
578                });
579                inner.check_block_count_changed();
580                inner.check_flow_changed();
581                let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
582                let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
583                inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
584            }
585            (count, inner.take_queued_events())
586        };
587        crate::inner::dispatch_queued_events(queued);
588        Ok(count)
589    }
590
591    // ── Resources ────────────────────────────────────────────
592
593    /// Add a resource (image, stylesheet) to the document.
594    pub fn add_resource(
595        &self,
596        resource_type: ResourceType,
597        name: &str,
598        mime_type: &str,
599        data: &[u8],
600    ) -> Result<()> {
601        let mut inner = self.inner.lock();
602        let dto = frontend::resource::dtos::CreateResourceDto {
603            created_at: Default::default(),
604            updated_at: Default::default(),
605            resource_type,
606            name: name.into(),
607            url: String::new(),
608            mime_type: mime_type.into(),
609            data_base64: BASE64.encode(data),
610        };
611        let created = resource_commands::create_resource(
612            &inner.ctx,
613            Some(inner.stack_id),
614            &dto,
615            inner.document_id,
616            -1,
617        )?;
618        inner.resource_cache.insert(name.to_string(), created.id);
619        Ok(())
620    }
621
622    /// Get a resource by name. Returns `None` if not found.
623    ///
624    /// Uses an internal cache to avoid scanning all resources on repeated lookups.
625    pub fn resource(&self, name: &str) -> Result<Option<Vec<u8>>> {
626        let mut inner = self.inner.lock();
627
628        // Fast path: check the name → ID cache.
629        if let Some(&id) = inner.resource_cache.get(name) {
630            if let Some(r) = resource_commands::get_resource(&inner.ctx, &id)? {
631                let bytes = BASE64.decode(&r.data_base64)?;
632                return Ok(Some(bytes));
633            }
634            // ID was stale — fall through to full scan.
635            inner.resource_cache.remove(name);
636        }
637
638        // Slow path: linear scan, then populate cache for the match.
639        let all = resource_commands::get_all_resource(&inner.ctx)?;
640        for r in &all {
641            if r.name == name {
642                inner.resource_cache.insert(name.to_string(), r.id);
643                let bytes = BASE64.decode(&r.data_base64)?;
644                return Ok(Some(bytes));
645            }
646        }
647        Ok(None)
648    }
649
650    // ── Undo / Redo ──────────────────────────────────────────
651
652    /// Undo the last operation.
653    pub fn undo(&self) -> Result<()> {
654        let queued = {
655            let mut inner = self.inner.lock();
656            let before = capture_block_state(&inner);
657            let result = undo_redo_commands::undo(&inner.ctx, Some(inner.stack_id));
658            inner.invalidate_text_cache();
659            result?;
660            inner.rehighlight_all();
661            emit_undo_redo_change_events(&mut inner, &before);
662            inner.check_block_count_changed();
663            inner.check_flow_changed();
664            let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
665            let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
666            inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
667            inner.take_queued_events()
668        };
669        crate::inner::dispatch_queued_events(queued);
670        Ok(())
671    }
672
673    /// Redo the last undone operation.
674    pub fn redo(&self) -> Result<()> {
675        let queued = {
676            let mut inner = self.inner.lock();
677            let before = capture_block_state(&inner);
678            let result = undo_redo_commands::redo(&inner.ctx, Some(inner.stack_id));
679            inner.invalidate_text_cache();
680            result?;
681            inner.rehighlight_all();
682            emit_undo_redo_change_events(&mut inner, &before);
683            inner.check_block_count_changed();
684            inner.check_flow_changed();
685            let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
686            let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
687            inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
688            inner.take_queued_events()
689        };
690        crate::inner::dispatch_queued_events(queued);
691        Ok(())
692    }
693
694    /// Returns true if there are operations that can be undone.
695    pub fn can_undo(&self) -> bool {
696        let inner = self.inner.lock();
697        undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id))
698    }
699
700    /// Returns true if there are operations that can be redone.
701    pub fn can_redo(&self) -> bool {
702        let inner = self.inner.lock();
703        undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id))
704    }
705
706    /// Clear all undo/redo history.
707    pub fn clear_undo_redo(&self) {
708        let inner = self.inner.lock();
709        undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
710    }
711
712    // ── Modified state ───────────────────────────────────────
713
714    /// Returns true if the document has been modified since creation or last reset.
715    pub fn is_modified(&self) -> bool {
716        self.inner.lock().modified
717    }
718
719    /// Set or clear the modified flag.
720    pub fn set_modified(&self, modified: bool) {
721        let queued = {
722            let mut inner = self.inner.lock();
723            if inner.modified != modified {
724                inner.modified = modified;
725                inner.queue_event(DocumentEvent::ModificationChanged(modified));
726            }
727            inner.take_queued_events()
728        };
729        crate::inner::dispatch_queued_events(queued);
730    }
731
732    // ── Document properties ──────────────────────────────────
733
734    /// Get the document title.
735    pub fn title(&self) -> String {
736        let inner = self.inner.lock();
737        document_commands::get_document(&inner.ctx, &inner.document_id)
738            .ok()
739            .flatten()
740            .map(|d| d.title)
741            .unwrap_or_default()
742    }
743
744    /// Set the document title.
745    pub fn set_title(&self, title: &str) -> Result<()> {
746        let inner = self.inner.lock();
747        let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
748            .ok_or_else(|| anyhow::anyhow!("document not found"))?;
749        let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
750        update.title = title.into();
751        document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
752        Ok(())
753    }
754
755    /// Get the text direction.
756    pub fn text_direction(&self) -> TextDirection {
757        let inner = self.inner.lock();
758        document_commands::get_document(&inner.ctx, &inner.document_id)
759            .ok()
760            .flatten()
761            .map(|d| d.text_direction)
762            .unwrap_or(TextDirection::LeftToRight)
763    }
764
765    /// Set the text direction.
766    pub fn set_text_direction(&self, direction: TextDirection) -> Result<()> {
767        let inner = self.inner.lock();
768        let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
769            .ok_or_else(|| anyhow::anyhow!("document not found"))?;
770        let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
771        update.text_direction = direction;
772        document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
773        Ok(())
774    }
775
776    /// Get the default wrap mode.
777    pub fn default_wrap_mode(&self) -> WrapMode {
778        let inner = self.inner.lock();
779        document_commands::get_document(&inner.ctx, &inner.document_id)
780            .ok()
781            .flatten()
782            .map(|d| d.default_wrap_mode)
783            .unwrap_or(WrapMode::WordWrap)
784    }
785
786    /// Set the default wrap mode.
787    pub fn set_default_wrap_mode(&self, mode: WrapMode) -> Result<()> {
788        let inner = self.inner.lock();
789        let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
790            .ok_or_else(|| anyhow::anyhow!("document not found"))?;
791        let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
792        update.default_wrap_mode = mode;
793        document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
794        Ok(())
795    }
796
797    // ── Event subscription ───────────────────────────────────
798
799    /// Subscribe to document events via callback.
800    ///
801    /// Callbacks are invoked **outside** the document lock (after the editing
802    /// operation completes and the lock is released). It is safe to call
803    /// `TextDocument` or `TextCursor` methods from within the callback without
804    /// risk of deadlock. However, keep callbacks lightweight — they run
805    /// synchronously on the calling thread and block the caller until they
806    /// return.
807    ///
808    /// Drop the returned [`Subscription`] to unsubscribe.
809    ///
810    /// # Breaking change (v0.0.6)
811    ///
812    /// The callback bound changed from `Send` to `Send + Sync` in v0.0.6
813    /// to support `Arc`-based dispatch. Callbacks that capture non-`Sync`
814    /// types (e.g., `Rc<T>`, `Cell<T>`) must be wrapped in a `Mutex`.
815    pub fn on_change<F>(&self, callback: F) -> Subscription
816    where
817        F: Fn(DocumentEvent) + Send + Sync + 'static,
818    {
819        let mut inner = self.inner.lock();
820        events::subscribe_inner(&mut inner, callback)
821    }
822
823    /// Return events accumulated since the last `poll_events()` call.
824    ///
825    /// This delivery path is independent of callback dispatch via
826    /// [`on_change`](Self::on_change) — using both simultaneously is safe
827    /// and each path sees every event exactly once.
828    pub fn poll_events(&self) -> Vec<DocumentEvent> {
829        let mut inner = self.inner.lock();
830        inner.drain_poll_events()
831    }
832
833    // ── Syntax highlighting ──────────────────────────────────
834
835    /// Attach a syntax highlighter to this document.
836    ///
837    /// Immediately re-highlights the entire document. Replaces any
838    /// previously attached highlighter. Pass `None` to remove the
839    /// highlighter and clear all highlight formatting.
840    pub fn set_syntax_highlighter(&self, highlighter: Option<Arc<dyn crate::SyntaxHighlighter>>) {
841        let mut inner = self.inner.lock();
842        match highlighter {
843            Some(hl) => {
844                inner.highlight = Some(crate::highlight::HighlightData {
845                    highlighter: hl,
846                    blocks: std::collections::HashMap::new(),
847                });
848                inner.rehighlight_all();
849            }
850            None => {
851                inner.highlight = None;
852            }
853        }
854    }
855
856    /// Re-highlight the entire document.
857    ///
858    /// Call this when the highlighter's rules change (e.g., new keywords
859    /// were added, spellcheck dictionary updated).
860    pub fn rehighlight(&self) {
861        let mut inner = self.inner.lock();
862        inner.rehighlight_all();
863    }
864
865    /// Re-highlight a single block and cascade to subsequent blocks if
866    /// the block state changes.
867    pub fn rehighlight_block(&self, block_id: usize) {
868        let mut inner = self.inner.lock();
869        inner.rehighlight_from_block(block_id);
870    }
871}
872
873impl Default for TextDocument {
874    fn default() -> Self {
875        Self::new()
876    }
877}
878
879// ── Undo/redo change detection helpers ─────────────────────────
880
881/// Lightweight block state for before/after comparison during undo/redo.
882struct UndoBlockState {
883    id: u64,
884    position: i64,
885    text_length: i64,
886    plain_text: String,
887    format: BlockFormat,
888}
889
890/// Capture the state of all blocks, sorted by document_position.
891fn capture_block_state(inner: &TextDocumentInner) -> Vec<UndoBlockState> {
892    let all_blocks =
893        frontend::commands::block_commands::get_all_block(&inner.ctx).unwrap_or_default();
894    let mut states: Vec<UndoBlockState> = all_blocks
895        .into_iter()
896        .map(|b| UndoBlockState {
897            id: b.id,
898            position: b.document_position,
899            text_length: b.text_length,
900            plain_text: b.plain_text.clone(),
901            format: BlockFormat::from(&b),
902        })
903        .collect();
904    states.sort_by_key(|s| s.position);
905    states
906}
907
908/// Compare block state before and after undo/redo and emit
909/// ContentsChanged / FormatChanged events for affected regions.
910fn emit_undo_redo_change_events(inner: &mut TextDocumentInner, before: &[UndoBlockState]) {
911    let after = capture_block_state(inner);
912
913    // Build a map of block id → state for the "before" set.
914    let before_map: std::collections::HashMap<u64, &UndoBlockState> =
915        before.iter().map(|s| (s.id, s)).collect();
916    let after_map: std::collections::HashMap<u64, &UndoBlockState> =
917        after.iter().map(|s| (s.id, s)).collect();
918
919    // Track the affected content region (earliest position, total old/new length).
920    let mut content_changed = false;
921    let mut earliest_pos: Option<usize> = None;
922    let mut old_end: usize = 0;
923    let mut new_end: usize = 0;
924    let mut blocks_affected: usize = 0;
925
926    let mut format_only_changes: Vec<(usize, usize)> = Vec::new(); // (position, length)
927
928    // Check blocks present in both before and after.
929    for after_state in &after {
930        if let Some(before_state) = before_map.get(&after_state.id) {
931            let text_changed = before_state.plain_text != after_state.plain_text
932                || before_state.text_length != after_state.text_length;
933            let format_changed = before_state.format != after_state.format;
934
935            if text_changed {
936                content_changed = true;
937                blocks_affected += 1;
938                let pos = after_state.position.max(0) as usize;
939                earliest_pos = Some(earliest_pos.map_or(pos, |p: usize| p.min(pos)));
940                old_end = old_end.max(
941                    before_state.position.max(0) as usize
942                        + before_state.text_length.max(0) as usize,
943                );
944                new_end = new_end.max(pos + after_state.text_length.max(0) as usize);
945            } else if format_changed {
946                let pos = after_state.position.max(0) as usize;
947                let len = after_state.text_length.max(0) as usize;
948                format_only_changes.push((pos, len));
949            }
950        } else {
951            // Block exists in after but not in before — new block from undo/redo.
952            content_changed = true;
953            blocks_affected += 1;
954            let pos = after_state.position.max(0) as usize;
955            earliest_pos = Some(earliest_pos.map_or(pos, |p: usize| p.min(pos)));
956            new_end = new_end.max(pos + after_state.text_length.max(0) as usize);
957        }
958    }
959
960    // Check blocks that were removed (present in before but not after).
961    for before_state in before {
962        if !after_map.contains_key(&before_state.id) {
963            content_changed = true;
964            blocks_affected += 1;
965            let pos = before_state.position.max(0) as usize;
966            earliest_pos = Some(earliest_pos.map_or(pos, |p: usize| p.min(pos)));
967            old_end = old_end.max(pos + before_state.text_length.max(0) as usize);
968        }
969    }
970
971    if content_changed {
972        let position = earliest_pos.unwrap_or(0);
973        inner.queue_event(DocumentEvent::ContentsChanged {
974            position,
975            chars_removed: old_end.saturating_sub(position),
976            chars_added: new_end.saturating_sub(position),
977            blocks_affected,
978        });
979    }
980
981    // Emit FormatChanged for blocks where only formatting changed (not content).
982    for (position, length) in format_only_changes {
983        inner.queue_event(DocumentEvent::FormatChanged {
984            position,
985            length,
986            kind: FormatChangeKind::Block,
987        });
988    }
989}
990
991// ── Flow helpers ──────────────────────────────────────────────
992
993/// Get the main frame ID for the document.
994fn get_main_frame_id(inner: &TextDocumentInner) -> frontend::common::types::EntityId {
995    // The document's first frame is the main frame.
996    let frames = frontend::commands::document_commands::get_document_relationship(
997        &inner.ctx,
998        &inner.document_id,
999        &frontend::document::dtos::DocumentRelationshipField::Frames,
1000    )
1001    .unwrap_or_default();
1002
1003    frames.first().copied().unwrap_or(0)
1004}
1005
1006// ── Long-operation event data helpers ─────────────────────────
1007
1008/// Parse progress JSON: `{"id":"...", "percentage": 50.0, "message": "..."}`
1009fn parse_progress_data(data: &Option<String>) -> (String, f64, String) {
1010    let Some(json) = data else {
1011        return (String::new(), 0.0, String::new());
1012    };
1013    let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
1014    let id = v["id"].as_str().unwrap_or_default().to_string();
1015    let pct = v["percentage"].as_f64().unwrap_or(0.0);
1016    let msg = v["message"].as_str().unwrap_or_default().to_string();
1017    (id, pct, msg)
1018}
1019
1020/// Parse completed/cancelled JSON: `{"id":"..."}`
1021fn parse_id_data(data: &Option<String>) -> String {
1022    let Some(json) = data else {
1023        return String::new();
1024    };
1025    let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
1026    v["id"].as_str().unwrap_or_default().to_string()
1027}
1028
1029/// Parse failed JSON: `{"id":"...", "error":"..."}`
1030fn parse_failed_data(data: &Option<String>) -> (String, String) {
1031    let Some(json) = data else {
1032        return (String::new(), "unknown error".into());
1033    };
1034    let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
1035    let id = v["id"].as_str().unwrap_or_default().to_string();
1036    let error = v["error"].as_str().unwrap_or("unknown error").to_string();
1037    (id, error)
1038}