use anyhow::Result;
use common::database::Store;
use common::database::rope_helpers::{
block_char_length, block_content_via_store, rope_delete_in_block, rope_insert_in_block,
};
use common::entities::Block;
use common::format_runs::{
ReplaceFormatPolicy, check_well_formed, logical_offset_to_byte, shift_images_for_delete,
shift_images_for_insert, shift_runs_for_replace,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RangeSpec {
pub position: usize,
pub length: usize,
pub replacement: String,
}
#[derive(Debug, Clone)]
pub(crate) struct PlannedEdit {
pub block_idx: usize,
pub block_offset: usize,
pub length: usize,
pub replacement: String,
}
#[derive(Debug, Default)]
pub(crate) struct Plan {
pub edits: Vec<PlannedEdit>,
pub skipped_cross_block: i64,
pub skipped_overlapping: i64,
}
pub(crate) fn plan(blocks: &[Block], specs: &[RangeSpec], store: &Store) -> Plan {
let mut sorted: Vec<&RangeSpec> = specs.iter().collect();
sorted.sort_by_key(|s| (s.position, s.length));
let mut out = Plan::default();
let mut accepted_end: Option<usize> = None;
for spec in sorted {
if let Some(end) = accepted_end
&& spec.position < end
{
out.skipped_overlapping += 1;
continue;
}
match resolve_in_block(blocks, spec.position, spec.length, store) {
Some((block_idx, block_offset)) => {
accepted_end = Some(spec.position + spec.length);
out.edits.push(PlannedEdit {
block_idx,
block_offset,
length: spec.length,
replacement: spec.replacement.clone(),
});
}
None => out.skipped_cross_block += 1,
}
}
out
}
fn resolve_in_block(
blocks: &[Block],
position: usize,
length: usize,
store: &Store,
) -> Option<(usize, usize)> {
for (i, block) in blocks.iter().enumerate() {
let start = block.document_position as usize;
let len = block_char_length(block, store) as usize;
let end = start + len;
let inside = position >= start && (position < end || (length == 0 && position == end));
if !inside {
continue;
}
let offset = position - start;
return (offset + length <= len).then_some((i, offset));
}
None
}
pub(crate) fn apply_in_block(
store: &Store,
block: &Block,
char_start: usize,
char_end: usize,
replacement: &str,
policy: ReplaceFormatPolicy,
) -> Result<Block> {
let images_before = store
.block_images
.read()
.get(&block.id)
.cloned()
.unwrap_or_default();
let block_text = block_content_via_store(block, store);
let byte_start = logical_offset_to_byte(&block_text, &images_before, char_start as i64);
let byte_end = logical_offset_to_byte(&block_text, &images_before, char_end as i64);
let mut new_plain = String::with_capacity(
block_text.len() - (byte_end - byte_start) as usize + replacement.len(),
);
new_plain.push_str(&block_text[..byte_start as usize]);
new_plain.push_str(replacement);
new_plain.push_str(&block_text[byte_end as usize..]);
let inserted_byte_len = replacement.len() as u32;
{
let mut runs_map = store.format_runs.write();
let runs = runs_map.entry(block.id).or_default();
shift_runs_for_replace(runs, byte_start, byte_end, inserted_byte_len, policy)?;
check_well_formed(runs, new_plain.len())?;
}
{
let mut images_map = store.block_images.write();
let images = images_map.entry(block.id).or_default();
shift_images_for_delete(images, byte_start, byte_end);
shift_images_for_insert(images, byte_start, inserted_byte_len);
}
rope_delete_in_block(store, block.id, byte_start, byte_end);
rope_insert_in_block(store, block.id, byte_start, replacement);
let mut updated = block.clone();
updated.updated_at = chrono::Utc::now();
Ok(updated)
}
pub(crate) fn rebase_positions(
blocks_in_order: &[Block],
delta_by_block_id: &std::collections::HashMap<common::types::EntityId, i64>,
) -> (Vec<Block>, i64) {
let mut to_update = Vec::new();
let mut cumulative: i64 = 0;
for block in blocks_in_order {
if cumulative != 0 {
let mut moved = block.clone();
moved.document_position += cumulative;
moved.updated_at = chrono::Utc::now();
to_update.push(moved);
}
cumulative += delta_by_block_id.get(&block.id).copied().unwrap_or(0);
}
(to_update, cumulative)
}
pub(crate) fn char_delta(edit: &PlannedEdit) -> i64 {
edit.replacement.chars().count() as i64 - edit.length as i64
}
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct Applied {
pub replacements_count: i64,
pub skipped_cross_block: i64,
pub skipped_overlapping: i64,
}