use std::sync::Arc;
use frontend::block::dtos::CreateBlockDto;
use frontend::commands::{block_commands, document_commands, frame_commands, undo_redo_commands};
use frontend::common::types::EntityId;
use frontend::document::dtos::UpdateDocumentDto;
use frontend::frame::dtos::UpdateFrameDto;
use crate::document::get_main_frame_id;
use crate::events::DocumentEvent;
use crate::inner::TextDocumentInner;
use crate::{DocumentError, Result, TextDocument};
impl TextDocument {
pub fn append_line(&self, text: &str) -> Result<usize> {
if text.contains('\n') {
return Err(DocumentError::InvalidArgument(
"append_line takes a single line; text must not contain '\\n'".into(),
));
}
let mut inner = self.inner.lock();
let frame_id = get_main_frame_id(&inner);
if frame_id == 0 {
return Err(DocumentError::InvalidArgument(
"document has no main frame".into(),
));
}
let atomic = Atomic::begin(&inner);
let appended = append_one(&mut inner, frame_id, text)?;
let new_count = commit_counts(&mut inner, 1, text.chars().count() as i64)?;
atomic.commit();
finish(&mut inner, appended.edit_pos, appended.chars_added, 1);
inner.queue_event(DocumentEvent::BlockCountChanged(new_count));
inner.queue_event(DocumentEvent::FlowElementsInserted {
flow_index: new_count - 1,
count: 1,
});
let queued = inner.take_queued_events();
drop(inner);
crate::inner::dispatch_queued_events(queued);
Ok(new_count)
}
pub fn append_lines<I, S>(&self, lines: I) -> Result<usize>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let lines: Vec<String> = lines.into_iter().map(|s| s.as_ref().to_owned()).collect();
if lines.iter().any(|l| l.contains('\n')) {
return Err(DocumentError::InvalidArgument(
"append_lines takes single lines; none may contain '\\n'".into(),
));
}
if lines.is_empty() {
let inner = self.inner.lock();
return Ok(current_block_count(&inner)? as usize);
}
let mut inner = self.inner.lock();
let frame_id = get_main_frame_id(&inner);
if frame_id == 0 {
return Err(DocumentError::InvalidArgument(
"document has no main frame".into(),
));
}
let atomic = Atomic::begin(&inner);
let mut edit_pos = None;
let mut chars_added = 0usize;
for line in &lines {
let appended = append_one(&mut inner, frame_id, line)?;
edit_pos.get_or_insert(appended.edit_pos);
chars_added += appended.chars_added;
}
let edit_pos = edit_pos.unwrap_or(0);
let chars: i64 = lines.iter().map(|l| l.chars().count() as i64).sum();
let new_count = commit_counts(&mut inner, lines.len() as i64, chars)?;
atomic.commit();
finish(&mut inner, edit_pos, chars_added, lines.len());
inner.queue_event(DocumentEvent::BlockCountChanged(new_count));
inner.queue_event(DocumentEvent::FlowElementsInserted {
flow_index: new_count - lines.len(),
count: lines.len(),
});
let queued = inner.take_queued_events();
drop(inner);
crate::inner::dispatch_queued_events(queued);
Ok(new_count)
}
pub fn truncate_front(&self, n: usize) -> Result<usize> {
if n == 0 {
return Ok(0);
}
let mut inner = self.inner.lock();
let frame_id = get_main_frame_id(&inner);
if frame_id == 0 {
return Err(DocumentError::InvalidArgument(
"document has no main frame".into(),
));
}
let stack = streaming_stack(&mut inner);
let (victims, chars_removed) = {
let frame = frame_commands::get_frame(&inner.ctx, &frame_id)?
.ok_or_else(|| DocumentError::InvalidArgument("main frame missing".into()))?;
let block_ids: Vec<EntityId> = frame
.child_order
.iter()
.filter(|e| **e > 0)
.map(|e| *e as EntityId)
.collect();
let take = n.min(block_ids.len().saturating_sub(1));
let victims: Vec<EntityId> = block_ids.into_iter().take(take).collect();
let mut chars: i64 = 0;
for id in &victims {
let block = block_commands::get_block(&inner.ctx, id)?.ok_or_else(|| {
DocumentError::InvalidArgument(format!(
"block {id} is referenced by the frame but does not exist"
))
})?;
let entity: common::entities::Block = block.into();
let store = inner.ctx.db_context.get_store();
chars += common::database::rope_helpers::block_char_length(&entity, store);
}
(victims, chars)
};
if victims.is_empty() {
return Ok(0);
}
let atomic = Atomic::begin(&inner);
{
let store = inner.ctx.db_context.get_store();
for id in &victims {
common::database::rope_helpers::rope_remove_block(store, *id);
}
}
for id in &victims {
block_commands::remove_block(&inner.ctx, Some(stack), id)?;
}
{
let frame = frame_commands::get_frame(&inner.ctx, &frame_id)?
.ok_or_else(|| DocumentError::InvalidArgument("main frame missing".into()))?;
let mut update: UpdateFrameDto = frame.into();
let evicted: std::collections::HashSet<i64> =
victims.iter().map(|id| *id as i64).collect();
let before = update.child_order.len();
update.child_order.retain(|e| !evicted.contains(e));
if update.child_order.len() != before {
frame_commands::update_frame(&inner.ctx, Some(stack), &update)?;
}
}
let removed = victims.len();
let new_count = commit_counts(&mut inner, -(removed as i64), -chars_removed)?;
atomic.commit();
undo_redo_commands::clear_stack(&inner.ctx, stack);
inner.invalidate_text_cache();
inner.adjust_cursors(0, chars_removed as usize + removed, 0);
inner.modified = true;
inner.queue_event(DocumentEvent::ContentsChanged {
position: 0,
chars_removed: chars_removed as usize + removed,
chars_added: 0,
blocks_affected: removed,
});
inner.queue_event(DocumentEvent::BlockCountChanged(new_count));
inner.queue_event(DocumentEvent::FlowElementsRemoved {
flow_index: 0,
count: removed,
});
let queued = inner.take_queued_events();
drop(inner);
crate::inner::dispatch_queued_events(queued);
Ok(removed)
}
}
fn current_block_count(inner: &TextDocumentInner) -> Result<i64> {
let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
.ok_or_else(|| DocumentError::InvalidArgument("document missing".into()))?;
Ok(doc.block_count)
}
fn streaming_stack(inner: &mut TextDocumentInner) -> u64 {
if let Some(id) = inner.streaming_stack_id {
return id;
}
let id = undo_redo_commands::create_new_stack(&inner.ctx);
inner.streaming_stack_id = Some(id);
id
}
struct Atomic {
store: Arc<common::database::Store>,
savepoint: Option<u64>,
}
impl Atomic {
fn begin(inner: &TextDocumentInner) -> Self {
let store = Arc::clone(inner.ctx.db_context.get_store());
let savepoint = Some(store.create_savepoint());
Self { store, savepoint }
}
fn commit(mut self) {
if let Some(sp) = self.savepoint.take() {
self.store.discard_savepoint(sp);
}
}
}
impl Drop for Atomic {
fn drop(&mut self) {
if let Some(sp) = self.savepoint.take() {
self.store.restore_savepoint(sp);
self.store.discard_savepoint(sp);
}
}
}
struct Appended {
edit_pos: usize,
chars_added: usize,
}
fn append_one(inner: &mut TextDocumentInner, frame_id: EntityId, text: &str) -> Result<Appended> {
let stack = streaming_stack(inner);
let frame = frame_commands::get_frame(&inner.ctx, &frame_id)?
.ok_or_else(|| DocumentError::InvalidArgument("main frame missing".into()))?;
let needs_boundary = frame.child_order.iter().any(|entry| *entry > 0);
let block = block_commands::create_block(
&inner.ctx,
Some(stack),
&CreateBlockDto::default(),
frame_id,
-1,
)?;
let appended = {
let store = inner.ctx.db_context.get_store();
let edit_pos = store.rope.read().len_chars();
if needs_boundary {
common::database::rope_helpers::rope_insert_block_boundary(store);
}
common::database::rope_helpers::rope_append_block(store, block.id, text);
Appended {
edit_pos,
chars_added: text.chars().count() + usize::from(needs_boundary),
}
};
let mut update: UpdateFrameDto = frame.into();
update.child_order.push(block.id as i64);
frame_commands::update_frame(&inner.ctx, Some(stack), &update)?;
Ok(appended)
}
fn commit_counts(
inner: &mut TextDocumentInner,
block_delta: i64,
char_delta: i64,
) -> Result<usize> {
let doc = document_commands::get_document(&inner.ctx, &inner.document_id)?
.ok_or_else(|| DocumentError::InvalidArgument("document missing".into()))?;
let stack = streaming_stack(inner);
let mut update: UpdateDocumentDto = doc.into();
update.block_count = (update.block_count + block_delta).max(0);
update.character_count = (update.character_count + char_delta).max(0);
let new_count = update.block_count as usize;
document_commands::update_document(&inner.ctx, Some(stack), &update)?;
Ok(new_count)
}
fn finish(inner: &mut TextDocumentInner, edit_pos: usize, added: usize, blocks_affected: usize) {
let stack = streaming_stack(inner);
undo_redo_commands::clear_stack(&inner.ctx, stack);
inner.invalidate_text_cache();
inner.adjust_cursors(edit_pos, 0, added);
inner.modified = true;
inner.queue_event(DocumentEvent::ContentsChanged {
position: edit_pos,
chars_removed: 0,
chars_added: added,
blocks_affected,
});
}