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 frontend::commands::{
12    document_commands, document_inspection_commands, document_io_commands,
13    document_search_commands, resource_commands, undo_redo_commands,
14};
15use crate::{ResourceType, TextDirection, WrapMode};
16
17use crate::convert::{self, to_i64, to_usize};
18use crate::cursor::TextCursor;
19use crate::events::{self, DocumentEvent, Subscription};
20use crate::inner::TextDocumentInner;
21use crate::operation::{DocxExportResult, HtmlImportResult, MarkdownImportResult, Operation};
22use crate::{BlockFormat, BlockInfo, DocumentStats, FindMatch, FindOptions};
23
24/// A rich text document.
25///
26/// Owns the backend (database, event hub, undo/redo manager) and provides
27/// document-level operations. All cursor-based editing goes through
28/// [`TextCursor`], obtained via [`cursor()`](TextDocument::cursor) or
29/// [`cursor_at()`](TextDocument::cursor_at).
30///
31/// Internally uses `Arc<Mutex<...>>` so that multiple [`TextCursor`]s can
32/// coexist and edit concurrently. Cloning a `TextDocument` creates a new
33/// handle to the **same** underlying document (like Qt's implicit sharing).
34#[derive(Clone)]
35pub struct TextDocument {
36    pub(crate) inner: Arc<Mutex<TextDocumentInner>>,
37}
38
39impl TextDocument {
40    // ── Construction ──────────────────────────────────────────
41
42    /// Create a new, empty document.
43    ///
44    /// # Panics
45    ///
46    /// Panics if the database context cannot be created (e.g. filesystem error).
47    /// Use [`TextDocument::try_new`] for a fallible alternative.
48    pub fn new() -> Self {
49        Self::try_new().expect("failed to initialize document")
50    }
51
52    /// Create a new, empty document, returning an error on failure.
53    pub fn try_new() -> Result<Self> {
54        let ctx = frontend::AppContext::new();
55        let doc_inner = TextDocumentInner::initialize(ctx)?;
56        Ok(Self {
57            inner: Arc::new(Mutex::new(doc_inner)),
58        })
59    }
60
61    // ── Whole-document content ────────────────────────────────
62
63    /// Replace the entire document with plain text. Clears undo history.
64    pub fn set_plain_text(&self, text: &str) -> Result<()> {
65        let queued = {
66            let mut inner = self.inner.lock();
67            let dto = frontend::document_io::ImportPlainTextDto {
68                plain_text: text.into(),
69            };
70            document_io_commands::import_plain_text(&inner.ctx, &dto)?;
71            undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
72            inner.invalidate_text_cache();
73            inner.queue_event(DocumentEvent::DocumentReset);
74            inner.take_queued_events()
75        };
76        crate::inner::dispatch_queued_events(queued);
77        Ok(())
78    }
79
80    /// Export the entire document as plain text.
81    pub fn to_plain_text(&self) -> Result<String> {
82        let mut inner = self.inner.lock();
83        Ok(inner.plain_text()?.to_string())
84    }
85
86    /// Replace the entire document with Markdown. Clears undo history.
87    ///
88    /// This is a **long operation**. Returns a typed [`Operation`] handle.
89    pub fn set_markdown(&self, markdown: &str) -> Result<Operation<MarkdownImportResult>> {
90        let mut inner = self.inner.lock();
91        inner.invalidate_text_cache();
92        let dto = frontend::document_io::ImportMarkdownDto {
93            markdown_text: markdown.into(),
94        };
95        let op_id = document_io_commands::import_markdown(&inner.ctx, &dto)?;
96        Ok(Operation::new(
97            op_id,
98            &inner.ctx,
99            Box::new(|ctx, id| {
100                document_io_commands::get_import_markdown_result(ctx, id)
101                    .ok()
102                    .flatten()
103                    .map(|r| {
104                        Ok(MarkdownImportResult {
105                            block_count: to_usize(r.block_count),
106                        })
107                    })
108            }),
109        ))
110    }
111
112    /// Export the entire document as Markdown.
113    pub fn to_markdown(&self) -> Result<String> {
114        let inner = self.inner.lock();
115        let dto = document_io_commands::export_markdown(&inner.ctx)?;
116        Ok(dto.markdown_text)
117    }
118
119    /// Replace the entire document with HTML. Clears undo history.
120    ///
121    /// This is a **long operation**. Returns a typed [`Operation`] handle.
122    pub fn set_html(&self, html: &str) -> Result<Operation<HtmlImportResult>> {
123        let mut inner = self.inner.lock();
124        inner.invalidate_text_cache();
125        let dto = frontend::document_io::ImportHtmlDto {
126            html_text: html.into(),
127        };
128        let op_id = document_io_commands::import_html(&inner.ctx, &dto)?;
129        Ok(Operation::new(
130            op_id,
131            &inner.ctx,
132            Box::new(|ctx, id| {
133                document_io_commands::get_import_html_result(ctx, id)
134                    .ok()
135                    .flatten()
136                    .map(|r| {
137                        Ok(HtmlImportResult {
138                            block_count: to_usize(r.block_count),
139                        })
140                    })
141            }),
142        ))
143    }
144
145    /// Export the entire document as HTML.
146    pub fn to_html(&self) -> Result<String> {
147        let inner = self.inner.lock();
148        let dto = document_io_commands::export_html(&inner.ctx)?;
149        Ok(dto.html_text)
150    }
151
152    /// Export the entire document as LaTeX.
153    pub fn to_latex(&self, document_class: &str, include_preamble: bool) -> Result<String> {
154        let inner = self.inner.lock();
155        let dto = frontend::document_io::ExportLatexDto {
156            document_class: document_class.into(),
157            include_preamble,
158        };
159        let result = document_io_commands::export_latex(&inner.ctx, &dto)?;
160        Ok(result.latex_text)
161    }
162
163    /// Export the entire document as DOCX to a file path.
164    ///
165    /// This is a **long operation**. Returns a typed [`Operation`] handle.
166    pub fn to_docx(&self, output_path: &str) -> Result<Operation<DocxExportResult>> {
167        let inner = self.inner.lock();
168        let dto = frontend::document_io::ExportDocxDto {
169            output_path: output_path.into(),
170        };
171        let op_id = document_io_commands::export_docx(&inner.ctx, &dto)?;
172        Ok(Operation::new(
173            op_id,
174            &inner.ctx,
175            Box::new(|ctx, id| {
176                document_io_commands::get_export_docx_result(ctx, id)
177                    .ok()
178                    .flatten()
179                    .map(|r| {
180                        Ok(DocxExportResult {
181                            file_path: r.file_path,
182                            paragraph_count: to_usize(r.paragraph_count),
183                        })
184                    })
185            }),
186        ))
187    }
188
189    /// Clear all document content and reset to an empty state.
190    pub fn clear(&self) -> Result<()> {
191        let queued = {
192            let mut inner = self.inner.lock();
193            let dto = frontend::document_io::ImportPlainTextDto {
194                plain_text: String::new(),
195            };
196            document_io_commands::import_plain_text(&inner.ctx, &dto)?;
197            undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
198            inner.invalidate_text_cache();
199            inner.queue_event(DocumentEvent::DocumentReset);
200            inner.take_queued_events()
201        };
202        crate::inner::dispatch_queued_events(queued);
203        Ok(())
204    }
205
206    // ── Cursor factory ───────────────────────────────────────
207
208    /// Create a cursor at position 0.
209    pub fn cursor(&self) -> TextCursor {
210        self.cursor_at(0)
211    }
212
213    /// Create a cursor at the given position.
214    pub fn cursor_at(&self, position: usize) -> TextCursor {
215        let data = {
216            let mut inner = self.inner.lock();
217            inner.register_cursor(position)
218        };
219        TextCursor {
220            doc: self.inner.clone(),
221            data,
222        }
223    }
224
225    // ── Document queries ─────────────────────────────────────
226
227    /// Get document statistics. O(1) — reads cached values.
228    pub fn stats(&self) -> DocumentStats {
229        let inner = self.inner.lock();
230        let dto = document_inspection_commands::get_document_stats(&inner.ctx)
231            .expect("get_document_stats should not fail");
232        DocumentStats::from(&dto)
233    }
234
235    /// Get the total character count. O(1) — reads cached value.
236    pub fn character_count(&self) -> usize {
237        let inner = self.inner.lock();
238        let dto = document_inspection_commands::get_document_stats(&inner.ctx)
239            .expect("get_document_stats should not fail");
240        to_usize(dto.character_count)
241    }
242
243    /// Get the number of blocks (paragraphs). O(1) — reads cached value.
244    pub fn block_count(&self) -> usize {
245        let inner = self.inner.lock();
246        let dto = document_inspection_commands::get_document_stats(&inner.ctx)
247            .expect("get_document_stats should not fail");
248        to_usize(dto.block_count)
249    }
250
251    /// Returns true if the document has no text content.
252    pub fn is_empty(&self) -> bool {
253        self.character_count() == 0
254    }
255
256    /// Get text at a position for a given length.
257    pub fn text_at(&self, position: usize, length: usize) -> Result<String> {
258        let inner = self.inner.lock();
259        let dto = frontend::document_inspection::GetTextAtPositionDto {
260            position: to_i64(position),
261            length: to_i64(length),
262        };
263        let result = document_inspection_commands::get_text_at_position(&inner.ctx, &dto)?;
264        Ok(result.text)
265    }
266
267    /// Get info about the block at a position. O(log n).
268    pub fn block_at(&self, position: usize) -> Result<BlockInfo> {
269        let inner = self.inner.lock();
270        let dto = frontend::document_inspection::GetBlockAtPositionDto {
271            position: to_i64(position),
272        };
273        let result = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)?;
274        Ok(BlockInfo::from(&result))
275    }
276
277    /// Get the block format at a position.
278    pub fn block_format_at(&self, position: usize) -> Result<BlockFormat> {
279        let inner = self.inner.lock();
280        let dto = frontend::document_inspection::GetBlockAtPositionDto {
281            position: to_i64(position),
282        };
283        let block_info = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)?;
284        let block_id = block_info.block_id;
285        let block_id = block_id as u64;
286        let block_dto = frontend::commands::block_commands::get_block(&inner.ctx, &block_id)?
287            .ok_or_else(|| anyhow::anyhow!("block not found"))?;
288        Ok(BlockFormat::from(&block_dto))
289    }
290
291    // ── Search ───────────────────────────────────────────────
292
293    /// Find the next (or previous) occurrence. Returns `None` if not found.
294    pub fn find(
295        &self,
296        query: &str,
297        from: usize,
298        options: &FindOptions,
299    ) -> Result<Option<FindMatch>> {
300        let inner = self.inner.lock();
301        let dto = options.to_find_text_dto(query, from);
302        let result = document_search_commands::find_text(&inner.ctx, &dto)?;
303        Ok(convert::find_result_to_match(&result))
304    }
305
306    /// Find all occurrences.
307    pub fn find_all(&self, query: &str, options: &FindOptions) -> Result<Vec<FindMatch>> {
308        let inner = self.inner.lock();
309        let dto = options.to_find_all_dto(query);
310        let result = document_search_commands::find_all(&inner.ctx, &dto)?;
311        Ok(convert::find_all_to_matches(&result))
312    }
313
314    /// Replace occurrences. Returns the number of replacements. Undoable.
315    pub fn replace_text(
316        &self,
317        query: &str,
318        replacement: &str,
319        replace_all: bool,
320        options: &FindOptions,
321    ) -> Result<usize> {
322        let mut inner = self.inner.lock();
323        let dto = options.to_replace_dto(query, replacement, replace_all);
324        let result =
325            document_search_commands::replace_text(&inner.ctx, Some(inner.stack_id), &dto)?;
326        inner.invalidate_text_cache();
327        Ok(to_usize(result.replacements_count))
328    }
329
330    // ── Resources ────────────────────────────────────────────
331
332    /// Add a resource (image, stylesheet) to the document.
333    pub fn add_resource(
334        &self,
335        resource_type: ResourceType,
336        name: &str,
337        mime_type: &str,
338        data: &[u8],
339    ) -> Result<()> {
340        let mut inner = self.inner.lock();
341        let dto = frontend::resource::dtos::CreateResourceDto {
342            created_at: Default::default(),
343            updated_at: Default::default(),
344            resource_type,
345            name: name.into(),
346            url: String::new(),
347            mime_type: mime_type.into(),
348            data_base64: BASE64.encode(data),
349        };
350        let created = resource_commands::create_resource(
351            &inner.ctx,
352            Some(inner.stack_id),
353            &dto,
354            inner.document_id,
355            -1,
356        )?;
357        inner.resource_cache.insert(name.to_string(), created.id);
358        Ok(())
359    }
360
361    /// Get a resource by name. Returns `None` if not found.
362    ///
363    /// Uses an internal cache to avoid scanning all resources on repeated lookups.
364    pub fn resource(&self, name: &str) -> Result<Option<Vec<u8>>> {
365        let mut inner = self.inner.lock();
366
367        // Fast path: check the name → ID cache.
368        if let Some(&id) = inner.resource_cache.get(name) {
369            if let Some(r) = resource_commands::get_resource(&inner.ctx, &id)? {
370                let bytes = BASE64.decode(&r.data_base64)?;
371                return Ok(Some(bytes));
372            }
373            // ID was stale — fall through to full scan.
374            inner.resource_cache.remove(name);
375        }
376
377        // Slow path: linear scan, then populate cache for the match.
378        let all = resource_commands::get_all_resource(&inner.ctx)?;
379        for r in &all {
380            if r.name == name {
381                inner.resource_cache.insert(name.to_string(), r.id);
382                let bytes = BASE64.decode(&r.data_base64)?;
383                return Ok(Some(bytes));
384            }
385        }
386        Ok(None)
387    }
388
389    // ── Undo / Redo ──────────────────────────────────────────
390
391    /// Undo the last operation.
392    pub fn undo(&self) -> Result<()> {
393        let mut inner = self.inner.lock();
394        let result = undo_redo_commands::undo(&inner.ctx, Some(inner.stack_id));
395        inner.invalidate_text_cache();
396        result
397    }
398
399    /// Redo the last undone operation.
400    pub fn redo(&self) -> Result<()> {
401        let mut inner = self.inner.lock();
402        let result = undo_redo_commands::redo(&inner.ctx, Some(inner.stack_id));
403        inner.invalidate_text_cache();
404        result
405    }
406
407    /// Returns true if there are operations that can be undone.
408    pub fn can_undo(&self) -> bool {
409        let inner = self.inner.lock();
410        undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id))
411    }
412
413    /// Returns true if there are operations that can be redone.
414    pub fn can_redo(&self) -> bool {
415        let inner = self.inner.lock();
416        undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id))
417    }
418
419    /// Clear all undo/redo history.
420    pub fn clear_undo_redo(&self) {
421        let inner = self.inner.lock();
422        undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
423    }
424
425    // ── Modified state ───────────────────────────────────────
426
427    /// Returns true if the document has been modified since creation or last reset.
428    pub fn is_modified(&self) -> bool {
429        self.inner.lock().modified
430    }
431
432    /// Set or clear the modified flag.
433    pub fn set_modified(&self, modified: bool) {
434        let queued = {
435            let mut inner = self.inner.lock();
436            if inner.modified != modified {
437                inner.modified = modified;
438                inner.queue_event(DocumentEvent::ModificationChanged(modified));
439            }
440            inner.take_queued_events()
441        };
442        crate::inner::dispatch_queued_events(queued);
443    }
444
445    // ── Document properties ──────────────────────────────────
446
447    /// Get the document title.
448    pub fn title(&self) -> String {
449        let inner = self.inner.lock();
450        document_commands::get_document(&inner.ctx, &inner.document_id)
451            .ok()
452            .flatten()
453            .map(|d| d.title)
454            .unwrap_or_default()
455    }
456
457    /// Set the document title.
458    pub fn set_title(&self, title: &str) -> Result<()> {
459        let inner = self.inner.lock();
460        let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
461            .ok_or_else(|| anyhow::anyhow!("document not found"))?;
462        let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
463        update.title = title.into();
464        document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
465        Ok(())
466    }
467
468    /// Get the text direction.
469    pub fn text_direction(&self) -> TextDirection {
470        let inner = self.inner.lock();
471        document_commands::get_document(&inner.ctx, &inner.document_id)
472            .ok()
473            .flatten()
474            .map(|d| d.text_direction)
475            .unwrap_or(TextDirection::LeftToRight)
476    }
477
478    /// Set the text direction.
479    pub fn set_text_direction(&self, direction: TextDirection) -> Result<()> {
480        let inner = self.inner.lock();
481        let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
482            .ok_or_else(|| anyhow::anyhow!("document not found"))?;
483        let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
484        update.text_direction = direction;
485        document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
486        Ok(())
487    }
488
489    /// Get the default wrap mode.
490    pub fn default_wrap_mode(&self) -> WrapMode {
491        let inner = self.inner.lock();
492        document_commands::get_document(&inner.ctx, &inner.document_id)
493            .ok()
494            .flatten()
495            .map(|d| d.default_wrap_mode)
496            .unwrap_or(WrapMode::WordWrap)
497    }
498
499    /// Set the default wrap mode.
500    pub fn set_default_wrap_mode(&self, mode: WrapMode) -> Result<()> {
501        let inner = self.inner.lock();
502        let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
503            .ok_or_else(|| anyhow::anyhow!("document not found"))?;
504        let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
505        update.default_wrap_mode = mode;
506        document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
507        Ok(())
508    }
509
510    // ── Event subscription ───────────────────────────────────
511
512    /// Subscribe to document events via callback.
513    ///
514    /// Callbacks are invoked **outside** the document lock (after the editing
515    /// operation completes and the lock is released). It is safe to call
516    /// `TextDocument` or `TextCursor` methods from within the callback without
517    /// risk of deadlock. However, keep callbacks lightweight — they run
518    /// synchronously on the calling thread and block the caller until they
519    /// return.
520    ///
521    /// Drop the returned [`Subscription`] to unsubscribe.
522    ///
523    /// # Breaking change (v0.0.6)
524    ///
525    /// The callback bound changed from `Send` to `Send + Sync` in v0.0.6
526    /// to support `Arc`-based dispatch. Callbacks that capture non-`Sync`
527    /// types (e.g., `Rc<T>`, `Cell<T>`) must be wrapped in a `Mutex`.
528    pub fn on_change<F>(&self, callback: F) -> Subscription
529    where
530        F: Fn(DocumentEvent) + Send + Sync + 'static,
531    {
532        let mut inner = self.inner.lock();
533        events::subscribe_inner(&mut inner, callback)
534    }
535
536    /// Return events accumulated since the last `poll_events()` call.
537    ///
538    /// This delivery path is independent of callback dispatch via
539    /// [`on_change`](Self::on_change) — using both simultaneously is safe
540    /// and each path sees every event exactly once.
541    pub fn poll_events(&self) -> Vec<DocumentEvent> {
542        let mut inner = self.inner.lock();
543        inner.drain_poll_events()
544    }
545}
546
547impl Default for TextDocument {
548    fn default() -> Self {
549        Self::new()
550    }
551}