use crate::database::Store;
use crate::database::block_offset_index::OffsetMarker;
use crate::entities::Block;
use crate::types::EntityId;
pub fn block_content_via_store(block: &Block, store: &Store) -> String {
let offsets = store.block_offsets.read().unwrap();
let marker = OffsetMarker::Block(block.id);
let Some((bs, be, has_successor)) = offsets.range_with_successor(marker) else {
return String::new();
};
let content_end = if has_successor && be > bs { be - 1 } else { be };
drop(offsets);
let rope = store.rope.read().unwrap();
rope.byte_slice(bs as usize..content_end as usize)
.to_string()
}
pub fn block_char_length(block: &Block, store: &Store) -> i64 {
let offsets = store.block_offsets.read().unwrap();
let marker = OffsetMarker::Block(block.id);
let Some((bs, be, has_successor)) = offsets.range_with_successor(marker) else {
return 0;
};
let content_end_bytes = if has_successor && be > bs { be - 1 } else { be };
drop(offsets);
let rope = store.rope.read().unwrap();
let char_start = rope.byte_to_char(bs as usize);
let char_end = rope.byte_to_char(content_end_bytes as usize);
(char_end - char_start) as i64
}
pub fn block_document_position(block: &Block, store: &Store) -> i64 {
let offsets = store.block_offsets.read().unwrap();
let Some((byte_start, _)) = offsets.range_of_block(block.id) else {
return block.document_position;
};
drop(offsets);
let rope = store.rope.read().unwrap();
rope.byte_to_char(byte_start as usize) as i64
}
pub fn rope_positions_match_flow(store: &Store) -> bool {
let offsets = store.block_offsets.read().unwrap();
if offsets.table_anchor_count() > 0 {
return false;
}
let indexed_block_count = offsets.entries.len();
drop(offsets);
let total_block_count = store.blocks.read().unwrap().len();
indexed_block_count == total_block_count
}
pub fn find_block_at_char_position(store: &Store, position: i64) -> Option<(EntityId, i64, i64)> {
let offsets = store.block_offsets.read().unwrap();
if offsets.table_anchor_count() > 0 {
return None;
}
let indexed_block_count = offsets.entries.len();
let total_block_count = store.blocks.read().unwrap().len();
if indexed_block_count != total_block_count {
return None;
}
drop(offsets);
let rope = store.rope.read().unwrap();
let total_chars = rope.len_chars() as i64;
let pos_clamped = position.clamp(0, total_chars);
let abs_byte = rope.char_to_byte(pos_clamped as usize);
drop(rope);
let offsets = store.block_offsets.read().unwrap();
let block_id = offsets.marker_at_byte(abs_byte as u32)?.as_block()?;
let (bs, be, has_successor) = offsets.range_with_successor(OffsetMarker::Block(block_id))?;
let content_end = if has_successor && be > bs { be - 1 } else { be };
drop(offsets);
let rope = store.rope.read().unwrap();
let block_char_start = rope.byte_to_char(bs as usize) as i64;
let byte_for_char = std::cmp::min(abs_byte, content_end as usize);
let abs_char = rope.byte_to_char(byte_for_char) as i64;
let char_in_block = abs_char - block_char_start;
let _ = has_successor;
Some((block_id, char_in_block, block_char_start))
}
pub fn block_char_to_byte_in_block(
store: &Store,
block_id: EntityId,
char_offset: usize,
) -> (u32, usize) {
let offsets = store.block_offsets.read().unwrap();
let marker = OffsetMarker::Block(block_id);
let Some((bs, be, has_successor)) = offsets.range_with_successor(marker) else {
return (0, 0);
};
let content_end_bytes = if has_successor && be > bs { be - 1 } else { be };
let content_byte_len = (content_end_bytes - bs) as usize;
drop(offsets);
let rope = store.rope.read().unwrap();
let block_char_start = rope.byte_to_char(bs as usize);
let block_char_end = rope.byte_to_char(content_end_bytes as usize);
let block_char_len = block_char_end - block_char_start;
let clamped = std::cmp::min(char_offset, block_char_len);
let abs_byte = rope.char_to_byte(block_char_start + clamped);
let byte_in_block = (abs_byte - bs as usize) as u32;
(byte_in_block, content_byte_len)
}
pub fn rope_flat_text_if_simple(store: &Store, top_frame_count: usize) -> Option<String> {
if top_frame_count != 1 {
return None;
}
let offsets = store.block_offsets.read().unwrap();
let has_table = offsets
.entries
.iter()
.any(|(m, _)| matches!(m, OffsetMarker::TableAnchor(_)));
if has_table {
return None;
}
drop(offsets);
Some(store.rope.read().unwrap().to_string())
}
pub fn rope_reset(store: &Store) {
*store.rope.write().unwrap() = ropey::Rope::new();
*store.block_offsets.write().unwrap() =
crate::database::block_offset_index::BlockOffsetIndex::new();
}
pub fn rope_append_block(store: &Store, block_id: EntityId, text: &str) -> u32 {
let mut rope = store.rope.write().unwrap();
let byte_start = rope.len_bytes() as u32;
let char_end = rope.len_chars();
rope.insert(char_end, text);
let new_total = rope.len_bytes() as u32;
drop(rope);
let mut offsets = store.block_offsets.write().unwrap();
offsets.push_block(block_id, byte_start);
offsets.set_total_bytes(new_total);
byte_start
}
pub fn rope_insert_block_at(store: &Store, byte_pos: u32, block_id: EntityId, text: &str) {
let delta = (1 + text.len()) as i32;
let new_entry_vec_pos = {
let offsets = store.block_offsets.read().unwrap();
offsets
.entries
.iter()
.position(|(_, bs)| *bs > byte_pos)
.unwrap_or(offsets.entries.len())
};
{
let mut rope = store.rope.write().unwrap();
let char_idx = rope.byte_to_char(byte_pos as usize);
let mut combined = String::with_capacity(1 + text.len());
combined.push('\n');
combined.push_str(text);
rope.insert(char_idx, &combined);
}
let mut offsets = store.block_offsets.write().unwrap();
offsets.shift_after(byte_pos + 1, delta);
offsets.insert_at(
new_entry_vec_pos,
OffsetMarker::Block(block_id),
byte_pos + 1,
);
}
pub fn top_level_frame_end_byte(store: &Store, frame_id: EntityId) -> u32 {
let top_id = {
let frames = store.frames.read().unwrap();
let mut current = frame_id;
loop {
let Some(f) = frames.get(¤t) else {
return 0;
};
match f.parent_frame {
None => break current,
Some(p) => current = p,
}
}
};
let (_min, max) = compute_frame_byte_range_recursive(store, top_id);
max
}
pub fn rope_append_empty_block(store: &Store, block_id: EntityId) -> u32 {
let was_empty = store.rope.read().unwrap().len_bytes() == 0;
if !was_empty {
rope_insert_block_boundary(store);
}
let pos = store.rope.read().unwrap().len_bytes() as u32;
let mut offsets = store.block_offsets.write().unwrap();
offsets.push_block(block_id, pos);
offsets.set_total_bytes(pos);
pos
}
pub fn rope_insert_block_boundary(store: &Store) {
let mut rope = store.rope.write().unwrap();
let char_end = rope.len_chars();
rope.insert(char_end, "\n");
let new_total = rope.len_bytes() as u32;
drop(rope);
store
.block_offsets
.write()
.unwrap()
.set_total_bytes(new_total);
}
pub fn rope_insert_in_block(
store: &Store,
block_id: EntityId,
byte_offset_in_block: u32,
text: &str,
) {
let inserted_bytes = text.len() as u32;
if inserted_bytes == 0 {
return;
}
let block_byte_start = {
let offsets = store.block_offsets.read().unwrap();
let Some((start, _end)) = offsets.range_of_block(block_id) else {
return;
};
start
};
let rope_byte = block_byte_start + byte_offset_in_block;
{
let mut rope = store.rope.write().unwrap();
let char_idx = rope.byte_to_char(rope_byte as usize);
rope.insert(char_idx, text);
}
store
.block_offsets
.write()
.unwrap()
.shift_after(block_byte_start + 1, inserted_bytes as i32);
}
pub fn rope_split_block(
store: &Store,
current_block_id: EntityId,
byte_offset_in_block: u32,
new_block_id: EntityId,
) {
let current_marker = OffsetMarker::Block(current_block_id);
let (block_start, current_idx) = {
let offsets = store.block_offsets.read().unwrap();
let Some((start, _end)) = offsets.range_of(current_marker) else {
return;
};
let idx = offsets
.entries
.iter()
.position(|(m, _)| *m == current_marker)
.unwrap();
(start, idx)
};
let split_byte = block_start + byte_offset_in_block;
{
let mut rope = store.rope.write().unwrap();
let char_idx = rope.byte_to_char(split_byte as usize);
rope.insert(char_idx, "\n");
}
store
.block_offsets
.write()
.unwrap()
.shift_after(split_byte + 1, 1);
store.block_offsets.write().unwrap().insert_at(
current_idx + 1,
OffsetMarker::Block(new_block_id),
split_byte + 1,
);
}
pub fn rope_merge_block_range(
store: &Store,
start_block_id: EntityId,
byte_so_in_start: u32,
end_block_id: EntityId,
byte_eo_in_end: u32,
) {
let start_marker = OffsetMarker::Block(start_block_id);
let end_marker = OffsetMarker::Block(end_block_id);
let (start_block_byte, end_block_byte, start_idx, end_idx) = {
let offsets = store.block_offsets.read().unwrap();
let Some((sb, _)) = offsets.range_of(start_marker) else {
return;
};
let Some((eb, _)) = offsets.range_of(end_marker) else {
return;
};
let si = offsets
.entries
.iter()
.position(|(m, _)| *m == start_marker)
.unwrap();
let ei = offsets
.entries
.iter()
.position(|(m, _)| *m == end_marker)
.unwrap();
(sb, eb, si, ei)
};
if end_idx <= start_idx {
return;
}
let delete_start = start_block_byte + byte_so_in_start;
let delete_end = end_block_byte + byte_eo_in_end;
if delete_end <= delete_start {
return;
}
let deleted_bytes = delete_end - delete_start;
{
let mut rope = store.rope.write().unwrap();
let char_start = rope.byte_to_char(delete_start as usize);
let char_end = rope.byte_to_char(delete_end as usize);
rope.remove(char_start..char_end);
}
{
let mut offsets = store.block_offsets.write().unwrap();
offsets.drain_inclusive(start_idx + 1, end_idx);
}
store
.block_offsets
.write()
.unwrap()
.shift_after(delete_start + 1, -(deleted_bytes as i32));
}
pub fn rope_insert_table_anchor(
store: &Store,
table_id: EntityId,
target_block_id: EntityId,
after: bool,
) {
const SENTINEL: &str = "\u{FFFC}"; const SENTINEL_BYTES: u32 = 3;
let (insert_pos, target_idx, target_is_last) = {
let offsets = store.block_offsets.read().unwrap();
let target_marker = OffsetMarker::Block(target_block_id);
let Some((start, end)) = offsets.range_of(target_marker) else {
return;
};
let idx = offsets
.entries
.iter()
.position(|(m, _)| *m == target_marker)
.unwrap();
let is_last = idx + 1 == offsets.entries.len();
let pos = if after { end } else { start };
(pos, idx, is_last)
};
let (rope_inserted, marker_byte_start, new_entry_pos, shift_threshold, shift_delta) = if !after
{
("\u{FFFC}\n", insert_pos, target_idx, insert_pos, 4i32)
} else if !target_is_last {
("\u{FFFC}\n", insert_pos, target_idx + 1, insert_pos, 4i32)
} else {
(
"\n\u{FFFC}",
insert_pos + 1,
target_idx + 1,
insert_pos,
4i32,
)
};
{
let mut rope = store.rope.write().unwrap();
let char_idx = rope.byte_to_char(insert_pos as usize);
rope.insert(char_idx, rope_inserted);
}
store
.block_offsets
.write()
.unwrap()
.shift_after(shift_threshold, shift_delta);
store.block_offsets.write().unwrap().insert_at(
new_entry_pos,
OffsetMarker::TableAnchor(table_id),
marker_byte_start,
);
let _ = SENTINEL;
let _ = SENTINEL_BYTES;
}
pub fn rope_append_table_anchor(store: &Store, table_id: EntityId) {
let (anchor_byte_start, new_total) = {
let mut rope = store.rope.write().unwrap();
let was_empty = rope.len_bytes() == 0;
let char_end = rope.len_chars();
let to_insert = if was_empty { "\u{FFFC}" } else { "\n\u{FFFC}" };
rope.insert(char_end, to_insert);
let new_total = rope.len_bytes() as u32;
let anchor_byte_start = new_total - 3;
(anchor_byte_start, new_total)
};
let mut offsets = store.block_offsets.write().unwrap();
offsets.push(OffsetMarker::TableAnchor(table_id), anchor_byte_start);
offsets.set_total_bytes(new_total);
}
pub fn rope_remove_table_anchor(store: &Store, table_id: EntityId) {
let anchor_marker = OffsetMarker::TableAnchor(table_id);
let (anchor_byte_start, anchor_idx, anchor_is_last, has_predecessor) = {
let offsets = store.block_offsets.read().unwrap();
let Some((start, _end)) = offsets.range_of(anchor_marker) else {
return;
};
let idx = offsets
.entries
.iter()
.position(|(m, _)| *m == anchor_marker)
.unwrap();
let is_last = idx + 1 == offsets.entries.len();
let has_pred = idx > 0;
(start, idx, is_last, has_pred)
};
let (remove_start, remove_end) = if anchor_is_last && has_predecessor {
(anchor_byte_start - 1, anchor_byte_start + 3)
} else {
(anchor_byte_start, anchor_byte_start + 4)
};
{
let mut rope = store.rope.write().unwrap();
let char_start = rope.byte_to_char(remove_start as usize);
let char_end = rope.byte_to_char(remove_end as usize);
rope.remove(char_start..char_end);
}
{
let mut offsets = store.block_offsets.write().unwrap();
offsets.remove_at(anchor_idx);
}
store
.block_offsets
.write()
.unwrap()
.shift_after(remove_start, -4);
}
pub fn rope_remove_block(store: &Store, block_id: EntityId) {
let block_marker = OffsetMarker::Block(block_id);
let (block_start, block_end, idx, is_last, has_pred) = {
let offsets = store.block_offsets.read().unwrap();
let Some((start, end)) = offsets.range_of(block_marker) else {
return;
};
let idx = offsets
.entries
.iter()
.position(|(m, _)| *m == block_marker)
.unwrap();
let is_last = idx + 1 == offsets.entries.len();
let has_pred = idx > 0;
(start, end, idx, is_last, has_pred)
};
let (remove_start, remove_end) = if is_last && has_pred {
(block_start.saturating_sub(1), block_end)
} else {
(block_start, block_end)
};
if remove_end <= remove_start {
store.block_offsets.write().unwrap().remove_at(idx);
return;
}
let deleted_bytes = remove_end - remove_start;
{
let mut rope = store.rope.write().unwrap();
let char_start = rope.byte_to_char(remove_start as usize);
let char_end = rope.byte_to_char(remove_end as usize);
rope.remove(char_start..char_end);
}
store.block_offsets.write().unwrap().remove_at(idx);
store
.block_offsets
.write()
.unwrap()
.shift_after(remove_end, -(deleted_bytes as i32));
}
pub fn rope_replace_block_content(store: &Store, block_id: EntityId, new_text: &str) {
let (block_byte_start, content_bytes) = {
let offsets = store.block_offsets.read().unwrap();
let Some((start, end)) = offsets.range_of_block(block_id) else {
return;
};
let total = offsets.total_bytes();
let has_trailing_boundary = end < total;
let content_bytes = if has_trailing_boundary {
end - start - 1
} else {
end - start
};
(start, content_bytes)
};
let new_bytes = new_text.len() as u32;
if content_bytes == 0 && new_bytes == 0 {
return;
}
let delta = new_bytes as i32 - content_bytes as i32;
{
let mut rope = store.rope.write().unwrap();
let char_start = rope.byte_to_char(block_byte_start as usize);
if content_bytes > 0 {
let char_end = rope.byte_to_char((block_byte_start + content_bytes) as usize);
rope.remove(char_start..char_end);
}
if new_bytes > 0 {
rope.insert(char_start, new_text);
}
}
if delta != 0 {
store
.block_offsets
.write()
.unwrap()
.shift_after(block_byte_start + 1, delta);
}
}
pub fn rope_delete_in_block(
store: &Store,
block_id: EntityId,
byte_start_in_block: u32,
byte_end_in_block: u32,
) {
if byte_end_in_block <= byte_start_in_block {
return;
}
let deleted_bytes = byte_end_in_block - byte_start_in_block;
let block_byte_start = {
let offsets = store.block_offsets.read().unwrap();
let Some((start, _end)) = offsets.range_of_block(block_id) else {
return;
};
start
};
let rope_byte_start = block_byte_start + byte_start_in_block;
let rope_byte_end = block_byte_start + byte_end_in_block;
{
let mut rope = store.rope.write().unwrap();
let char_start = rope.byte_to_char(rope_byte_start as usize);
let char_end = rope.byte_to_char(rope_byte_end as usize);
rope.remove(char_start..char_end);
}
store
.block_offsets
.write()
.unwrap()
.shift_after(block_byte_start + 1, -(deleted_bytes as i32));
}
pub fn recompute_all_frame_byte_ranges(store: &Store) {
let frame_ids: Vec<EntityId> = {
let frames = store.frames.read().unwrap();
frames.keys().copied().collect()
};
for fid in frame_ids {
let new_range = compute_frame_byte_range_recursive(store, fid);
let mut frames = store.frames.write().unwrap();
if let Some(f) = frames.get(&fid).cloned()
&& f.byte_range != new_range
{
let mut updated = f;
updated.byte_range = new_range;
frames.insert(fid, updated);
}
}
}
fn compute_frame_byte_range_recursive(store: &Store, frame_id: EntityId) -> (u32, u32) {
let mut bounds: Option<(u32, u32)> = None;
walk_frame_bounds(store, frame_id, &mut bounds);
bounds.unwrap_or((0, 0))
}
fn walk_frame_bounds(store: &Store, frame_id: EntityId, bounds: &mut Option<(u32, u32)>) {
fn merge(bounds: &mut Option<(u32, u32)>, s: u32, e: u32) {
*bounds = Some(match *bounds {
None => (s, e),
Some((min, max)) => (min.min(s), max.max(e)),
});
}
let (blocks, child_order, table_id) = {
let frames = store.frames.read().unwrap();
let Some(f) = frames.get(&frame_id) else {
return;
};
(f.blocks.clone(), f.child_order.clone(), f.table)
};
{
let offsets = store.block_offsets.read().unwrap();
for bid in &blocks {
if let Some((s, e)) = offsets.range_of_block(*bid) {
merge(bounds, s, e);
}
}
if let Some(tid) = table_id
&& let Some((s, e)) = offsets.range_of(OffsetMarker::TableAnchor(tid))
{
merge(bounds, s, e);
}
}
for entry in &child_order {
if *entry < 0 {
walk_frame_bounds(store, (-*entry) as EntityId, bounds);
}
}
if let Some(tid) = table_id {
let cell_ids: Vec<EntityId> = {
let tables = store.tables.read().unwrap();
tables
.get(&tid)
.map(|t| t.cells.clone())
.unwrap_or_default()
};
for cell_id in &cell_ids {
let cell_frame_id = {
let cells = store.table_cells.read().unwrap();
cells.get(cell_id).and_then(|c| c.cell_frame)
};
if let Some(cfid) = cell_frame_id {
walk_frame_bounds(store, cfid, bounds);
}
}
}
}