1use 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#[derive(Clone)]
35pub struct TextDocument {
36 pub(crate) inner: Arc<Mutex<TextDocumentInner>>,
37}
38
39impl TextDocument {
40 pub fn new() -> Self {
49 Self::try_new().expect("failed to initialize document")
50 }
51
52 pub fn try_new() -> Result<Self> {
54 let ctx = frontend::AppContext::new();
55 let doc_inner = TextDocumentInner::initialize(ctx)?;
56 let inner = Arc::new(Mutex::new(doc_inner));
57
58 Self::subscribe_long_operation_events(&inner);
60
61 Ok(Self { inner })
62 }
63
64 fn subscribe_long_operation_events(inner: &Arc<Mutex<TextDocumentInner>>) {
66 use frontend::common::event::{LongOperationEvent as LOE, Origin};
67
68 let weak = Arc::downgrade(inner);
69 {
70 let locked = inner.lock();
71 let w = weak.clone();
73 locked.event_client.subscribe(
74 Origin::LongOperation(LOE::Progress),
75 move |event| {
76 if let Some(inner) = w.upgrade() {
77 let (op_id, percent, message) = parse_progress_data(&event.data);
78 let mut inner = inner.lock();
79 inner.queue_event(DocumentEvent::LongOperationProgress {
80 operation_id: op_id,
81 percent,
82 message,
83 });
84 }
85 },
86 );
87
88 let w = weak.clone();
90 locked.event_client.subscribe(
91 Origin::LongOperation(LOE::Completed),
92 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.queue_event(DocumentEvent::LongOperationFinished {
99 operation_id: op_id,
100 success: true,
101 error: None,
102 });
103 }
104 },
105 );
106
107 let w = weak.clone();
109 locked.event_client.subscribe(
110 Origin::LongOperation(LOE::Cancelled),
111 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
124 locked.event_client.subscribe(
126 Origin::LongOperation(LOE::Failed),
127 move |event| {
128 if let Some(inner) = weak.upgrade() {
129 let (op_id, error) = parse_failed_data(&event.data);
130 let mut inner = inner.lock();
131 inner.queue_event(DocumentEvent::LongOperationFinished {
132 operation_id: op_id,
133 success: false,
134 error: Some(error),
135 });
136 }
137 },
138 );
139 }
140 }
141
142 pub fn set_plain_text(&self, text: &str) -> Result<()> {
146 let queued = {
147 let mut inner = self.inner.lock();
148 let dto = frontend::document_io::ImportPlainTextDto {
149 plain_text: text.into(),
150 };
151 document_io_commands::import_plain_text(&inner.ctx, &dto)?;
152 undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
153 inner.invalidate_text_cache();
154 inner.queue_event(DocumentEvent::DocumentReset);
155 inner.check_block_count_changed();
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 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 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 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 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 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 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 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 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.queue_event(DocumentEvent::DocumentReset);
286 inner.check_block_count_changed();
287 inner.queue_event(DocumentEvent::UndoRedoChanged {
288 can_undo: false,
289 can_redo: false,
290 });
291 inner.take_queued_events()
292 };
293 crate::inner::dispatch_queued_events(queued);
294 Ok(())
295 }
296
297 pub fn cursor(&self) -> TextCursor {
301 self.cursor_at(0)
302 }
303
304 pub fn cursor_at(&self, position: usize) -> TextCursor {
306 let data = {
307 let mut inner = self.inner.lock();
308 inner.register_cursor(position)
309 };
310 TextCursor {
311 doc: self.inner.clone(),
312 data,
313 }
314 }
315
316 pub fn stats(&self) -> DocumentStats {
320 let inner = self.inner.lock();
321 let dto = document_inspection_commands::get_document_stats(&inner.ctx)
322 .expect("get_document_stats should not fail");
323 DocumentStats::from(&dto)
324 }
325
326 pub fn character_count(&self) -> usize {
328 let inner = self.inner.lock();
329 let dto = document_inspection_commands::get_document_stats(&inner.ctx)
330 .expect("get_document_stats should not fail");
331 to_usize(dto.character_count)
332 }
333
334 pub fn block_count(&self) -> usize {
336 let inner = self.inner.lock();
337 let dto = document_inspection_commands::get_document_stats(&inner.ctx)
338 .expect("get_document_stats should not fail");
339 to_usize(dto.block_count)
340 }
341
342 pub fn is_empty(&self) -> bool {
344 self.character_count() == 0
345 }
346
347 pub fn text_at(&self, position: usize, length: usize) -> Result<String> {
349 let inner = self.inner.lock();
350 let dto = frontend::document_inspection::GetTextAtPositionDto {
351 position: to_i64(position),
352 length: to_i64(length),
353 };
354 let result = document_inspection_commands::get_text_at_position(&inner.ctx, &dto)?;
355 Ok(result.text)
356 }
357
358 pub fn block_at(&self, position: usize) -> Result<BlockInfo> {
360 let inner = self.inner.lock();
361 let dto = frontend::document_inspection::GetBlockAtPositionDto {
362 position: to_i64(position),
363 };
364 let result = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)?;
365 Ok(BlockInfo::from(&result))
366 }
367
368 pub fn block_format_at(&self, position: usize) -> Result<BlockFormat> {
370 let inner = self.inner.lock();
371 let dto = frontend::document_inspection::GetBlockAtPositionDto {
372 position: to_i64(position),
373 };
374 let block_info = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)?;
375 let block_id = block_info.block_id;
376 let block_id = block_id as u64;
377 let block_dto = frontend::commands::block_commands::get_block(&inner.ctx, &block_id)?
378 .ok_or_else(|| anyhow::anyhow!("block not found"))?;
379 Ok(BlockFormat::from(&block_dto))
380 }
381
382 pub fn find(
386 &self,
387 query: &str,
388 from: usize,
389 options: &FindOptions,
390 ) -> Result<Option<FindMatch>> {
391 let inner = self.inner.lock();
392 let dto = options.to_find_text_dto(query, from);
393 let result = document_search_commands::find_text(&inner.ctx, &dto)?;
394 Ok(convert::find_result_to_match(&result))
395 }
396
397 pub fn find_all(&self, query: &str, options: &FindOptions) -> Result<Vec<FindMatch>> {
399 let inner = self.inner.lock();
400 let dto = options.to_find_all_dto(query);
401 let result = document_search_commands::find_all(&inner.ctx, &dto)?;
402 Ok(convert::find_all_to_matches(&result))
403 }
404
405 pub fn replace_text(
407 &self,
408 query: &str,
409 replacement: &str,
410 replace_all: bool,
411 options: &FindOptions,
412 ) -> Result<usize> {
413 let (count, queued) = {
414 let mut inner = self.inner.lock();
415 let dto = options.to_replace_dto(query, replacement, replace_all);
416 let result =
417 document_search_commands::replace_text(&inner.ctx, Some(inner.stack_id), &dto)?;
418 let count = to_usize(result.replacements_count);
419 inner.invalidate_text_cache();
420 if count > 0 {
421 inner.modified = true;
422 inner.queue_event(DocumentEvent::ContentsChanged {
423 position: 0,
424 chars_removed: 0,
425 chars_added: 0,
426 blocks_affected: 0,
427 });
428 inner.check_block_count_changed();
429 let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
430 let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
431 inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
432 }
433 (count, inner.take_queued_events())
434 };
435 crate::inner::dispatch_queued_events(queued);
436 Ok(count)
437 }
438
439 pub fn add_resource(
443 &self,
444 resource_type: ResourceType,
445 name: &str,
446 mime_type: &str,
447 data: &[u8],
448 ) -> Result<()> {
449 let mut inner = self.inner.lock();
450 let dto = frontend::resource::dtos::CreateResourceDto {
451 created_at: Default::default(),
452 updated_at: Default::default(),
453 resource_type,
454 name: name.into(),
455 url: String::new(),
456 mime_type: mime_type.into(),
457 data_base64: BASE64.encode(data),
458 };
459 let created = resource_commands::create_resource(
460 &inner.ctx,
461 Some(inner.stack_id),
462 &dto,
463 inner.document_id,
464 -1,
465 )?;
466 inner.resource_cache.insert(name.to_string(), created.id);
467 Ok(())
468 }
469
470 pub fn resource(&self, name: &str) -> Result<Option<Vec<u8>>> {
474 let mut inner = self.inner.lock();
475
476 if let Some(&id) = inner.resource_cache.get(name) {
478 if let Some(r) = resource_commands::get_resource(&inner.ctx, &id)? {
479 let bytes = BASE64.decode(&r.data_base64)?;
480 return Ok(Some(bytes));
481 }
482 inner.resource_cache.remove(name);
484 }
485
486 let all = resource_commands::get_all_resource(&inner.ctx)?;
488 for r in &all {
489 if r.name == name {
490 inner.resource_cache.insert(name.to_string(), r.id);
491 let bytes = BASE64.decode(&r.data_base64)?;
492 return Ok(Some(bytes));
493 }
494 }
495 Ok(None)
496 }
497
498 pub fn undo(&self) -> Result<()> {
502 let queued = {
503 let mut inner = self.inner.lock();
504 let result = undo_redo_commands::undo(&inner.ctx, Some(inner.stack_id));
505 inner.invalidate_text_cache();
506 result?;
507 inner.check_block_count_changed();
508 let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
509 let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
510 inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
511 inner.take_queued_events()
512 };
513 crate::inner::dispatch_queued_events(queued);
514 Ok(())
515 }
516
517 pub fn redo(&self) -> Result<()> {
519 let queued = {
520 let mut inner = self.inner.lock();
521 let result = undo_redo_commands::redo(&inner.ctx, Some(inner.stack_id));
522 inner.invalidate_text_cache();
523 result?;
524 inner.check_block_count_changed();
525 let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
526 let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
527 inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
528 inner.take_queued_events()
529 };
530 crate::inner::dispatch_queued_events(queued);
531 Ok(())
532 }
533
534 pub fn can_undo(&self) -> bool {
536 let inner = self.inner.lock();
537 undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id))
538 }
539
540 pub fn can_redo(&self) -> bool {
542 let inner = self.inner.lock();
543 undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id))
544 }
545
546 pub fn clear_undo_redo(&self) {
548 let inner = self.inner.lock();
549 undo_redo_commands::clear_stack(&inner.ctx, inner.stack_id);
550 }
551
552 pub fn is_modified(&self) -> bool {
556 self.inner.lock().modified
557 }
558
559 pub fn set_modified(&self, modified: bool) {
561 let queued = {
562 let mut inner = self.inner.lock();
563 if inner.modified != modified {
564 inner.modified = modified;
565 inner.queue_event(DocumentEvent::ModificationChanged(modified));
566 }
567 inner.take_queued_events()
568 };
569 crate::inner::dispatch_queued_events(queued);
570 }
571
572 pub fn title(&self) -> String {
576 let inner = self.inner.lock();
577 document_commands::get_document(&inner.ctx, &inner.document_id)
578 .ok()
579 .flatten()
580 .map(|d| d.title)
581 .unwrap_or_default()
582 }
583
584 pub fn set_title(&self, title: &str) -> Result<()> {
586 let inner = self.inner.lock();
587 let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
588 .ok_or_else(|| anyhow::anyhow!("document not found"))?;
589 let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
590 update.title = title.into();
591 document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
592 Ok(())
593 }
594
595 pub fn text_direction(&self) -> TextDirection {
597 let inner = self.inner.lock();
598 document_commands::get_document(&inner.ctx, &inner.document_id)
599 .ok()
600 .flatten()
601 .map(|d| d.text_direction)
602 .unwrap_or(TextDirection::LeftToRight)
603 }
604
605 pub fn set_text_direction(&self, direction: TextDirection) -> Result<()> {
607 let inner = self.inner.lock();
608 let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
609 .ok_or_else(|| anyhow::anyhow!("document not found"))?;
610 let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
611 update.text_direction = direction;
612 document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
613 Ok(())
614 }
615
616 pub fn default_wrap_mode(&self) -> WrapMode {
618 let inner = self.inner.lock();
619 document_commands::get_document(&inner.ctx, &inner.document_id)
620 .ok()
621 .flatten()
622 .map(|d| d.default_wrap_mode)
623 .unwrap_or(WrapMode::WordWrap)
624 }
625
626 pub fn set_default_wrap_mode(&self, mode: WrapMode) -> Result<()> {
628 let inner = self.inner.lock();
629 let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
630 .ok_or_else(|| anyhow::anyhow!("document not found"))?;
631 let mut update: frontend::document::dtos::UpdateDocumentDto = doc.into();
632 update.default_wrap_mode = mode;
633 document_commands::update_document(&inner.ctx, Some(inner.stack_id), &update)?;
634 Ok(())
635 }
636
637 pub fn on_change<F>(&self, callback: F) -> Subscription
656 where
657 F: Fn(DocumentEvent) + Send + Sync + 'static,
658 {
659 let mut inner = self.inner.lock();
660 events::subscribe_inner(&mut inner, callback)
661 }
662
663 pub fn poll_events(&self) -> Vec<DocumentEvent> {
669 let mut inner = self.inner.lock();
670 inner.drain_poll_events()
671 }
672}
673
674impl Default for TextDocument {
675 fn default() -> Self {
676 Self::new()
677 }
678}
679
680fn parse_progress_data(data: &Option<String>) -> (String, f64, String) {
684 let Some(json) = data else {
685 return (String::new(), 0.0, String::new());
686 };
687 let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
688 let id = v["id"].as_str().unwrap_or_default().to_string();
689 let pct = v["percentage"].as_f64().unwrap_or(0.0);
690 let msg = v["message"].as_str().unwrap_or_default().to_string();
691 (id, pct, msg)
692}
693
694fn parse_id_data(data: &Option<String>) -> String {
696 let Some(json) = data else {
697 return String::new();
698 };
699 let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
700 v["id"].as_str().unwrap_or_default().to_string()
701}
702
703fn parse_failed_data(data: &Option<String>) -> (String, String) {
705 let Some(json) = data else {
706 return (String::new(), "unknown error".into());
707 };
708 let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
709 let id = v["id"].as_str().unwrap_or_default().to_string();
710 let error = v["error"].as_str().unwrap_or("unknown error").to_string();
711 (id, error)
712}