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 Ok(Self {
57 inner: Arc::new(Mutex::new(doc_inner)),
58 })
59 }
60
61 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 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 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 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 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 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 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 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 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 pub fn cursor(&self) -> TextCursor {
210 self.cursor_at(0)
211 }
212
213 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 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 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 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 pub fn is_empty(&self) -> bool {
253 self.character_count() == 0
254 }
255
256 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 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 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 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 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 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 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 pub fn resource(&self, name: &str) -> Result<Option<Vec<u8>>> {
365 let mut inner = self.inner.lock();
366
367 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 inner.resource_cache.remove(name);
375 }
376
377 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 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 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 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 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 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 pub fn is_modified(&self) -> bool {
429 self.inner.lock().modified
430 }
431
432 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 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 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 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 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 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 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 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 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}