use std::sync::Arc;
use parking_lot::Mutex;
use frontend::commands::{block_commands, frame_commands, list_commands};
use frontend::common::format_runs::{FormatRun, ImageAnchor, synth_element_id};
use frontend::common::types::EntityId;
use crate::convert::to_usize;
use crate::flow::{BlockSnapshot, FragmentContent, ListInfo, TableCellContext, TableCellRef};
use crate::inner::TextDocumentInner;
use crate::text_frame::TextFrame;
use crate::text_list::TextList;
use crate::text_table::TextTable;
use crate::{BlockFormat, ListStyle, TextFormat};
#[derive(Clone)]
pub struct TextBlock {
pub(crate) doc: Arc<Mutex<TextDocumentInner>>,
pub(crate) block_id: usize,
}
impl TextBlock {
pub fn text(&self) -> String {
let inner = self.doc.lock();
let store = inner.ctx.db_context.get_store();
block_commands::get_block(&inner.ctx, &(self.block_id as u64))
.ok()
.flatten()
.map(|b| {
let entity: common::entities::Block = b.into();
common::database::rope_helpers::block_content_via_store(&entity, store)
})
.unwrap_or_default()
}
pub fn length(&self) -> usize {
let inner = self.doc.lock();
let store = inner.ctx.db_context.get_store();
block_commands::get_block(&inner.ctx, &(self.block_id as u64))
.ok()
.flatten()
.map(|b| {
let entity: common::entities::Block = b.into();
to_usize(common::database::rope_helpers::block_char_length(
&entity, store,
))
})
.unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
let inner = self.doc.lock();
let store = inner.ctx.db_context.get_store();
block_commands::get_block(&inner.ctx, &(self.block_id as u64))
.ok()
.flatten()
.map(|b| {
let entity: common::entities::Block = b.into();
common::database::rope_helpers::block_char_length(&entity, store) == 0
})
.unwrap_or(true)
}
pub fn is_valid(&self) -> bool {
let inner = self.doc.lock();
block_commands::get_block(&inner.ctx, &(self.block_id as u64))
.ok()
.flatten()
.is_some()
}
pub fn id(&self) -> usize {
self.block_id
}
pub fn position(&self) -> usize {
let inner = self.doc.lock();
let Some(mut dto) = block_commands::get_block(&inner.ctx, &(self.block_id as u64))
.ok()
.flatten()
else {
return 0;
};
let store = inner.ctx.db_context.get_store();
crate::inner::refresh_block_position(&mut dto, store);
to_usize(dto.document_position)
}
pub fn block_number(&self) -> usize {
let inner = self.doc.lock();
compute_block_number(&inner, self.block_id as u64)
}
pub fn next(&self) -> Option<TextBlock> {
let inner = self.doc.lock();
let all_blocks = block_commands::get_all_block(&inner.ctx).ok()?;
let mut sorted: Vec<_> = all_blocks.into_iter().collect();
let store = inner.ctx.db_context.get_store();
crate::inner::refresh_block_positions(&mut sorted, store);
sorted.sort_by_key(|b| b.document_position);
let idx = sorted.iter().position(|b| b.id == self.block_id as u64)?;
sorted.get(idx + 1).map(|b| TextBlock {
doc: Arc::clone(&self.doc),
block_id: b.id as usize,
})
}
pub fn previous(&self) -> Option<TextBlock> {
let inner = self.doc.lock();
let all_blocks = block_commands::get_all_block(&inner.ctx).ok()?;
let mut sorted: Vec<_> = all_blocks.into_iter().collect();
let store = inner.ctx.db_context.get_store();
crate::inner::refresh_block_positions(&mut sorted, store);
sorted.sort_by_key(|b| b.document_position);
let idx = sorted.iter().position(|b| b.id == self.block_id as u64)?;
if idx == 0 {
return None;
}
sorted.get(idx - 1).map(|b| TextBlock {
doc: Arc::clone(&self.doc),
block_id: b.id as usize,
})
}
pub fn frame(&self) -> TextFrame {
let inner = self.doc.lock();
let frame_id = find_parent_frame(&inner, self.block_id as u64);
TextFrame {
doc: Arc::clone(&self.doc),
frame_id: frame_id.map(|id| id as usize).unwrap_or(0),
}
}
pub fn table_cell(&self) -> Option<TableCellRef> {
let inner = self.doc.lock();
let frame_id = find_parent_frame(&inner, self.block_id as u64)?;
let frame_dto = frame_commands::get_frame(&inner.ctx, &frame_id)
.ok()
.flatten()?;
if let Some(table_entity_id) = frame_dto.table {
let table_dto =
frontend::commands::table_commands::get_table(&inner.ctx, &{ table_entity_id })
.ok()
.flatten()?;
for &cell_id in &table_dto.cells {
if let Some(cell_dto) =
frontend::commands::table_cell_commands::get_table_cell(&inner.ctx, &{
cell_id
})
.ok()
.flatten()
&& cell_dto.cell_frame == Some(frame_id)
{
return Some(TableCellRef {
table: TextTable {
doc: Arc::clone(&self.doc),
table_id: table_entity_id as usize,
},
row: to_usize(cell_dto.row),
column: to_usize(cell_dto.column),
});
}
}
}
let all_tables =
frontend::commands::table_commands::get_all_table(&inner.ctx).unwrap_or_default();
for table_dto in &all_tables {
for &cell_id in &table_dto.cells {
if let Some(cell_dto) =
frontend::commands::table_cell_commands::get_table_cell(&inner.ctx, &{
cell_id
})
.ok()
.flatten()
&& cell_dto.cell_frame == Some(frame_id)
{
return Some(TableCellRef {
table: TextTable {
doc: Arc::clone(&self.doc),
table_id: table_dto.id as usize,
},
row: to_usize(cell_dto.row),
column: to_usize(cell_dto.column),
});
}
}
}
None
}
pub fn block_format(&self) -> BlockFormat {
let inner = self.doc.lock();
block_commands::get_block(&inner.ctx, &(self.block_id as u64))
.ok()
.flatten()
.map(|b| BlockFormat::from(&b))
.unwrap_or_default()
}
pub fn char_format_at(&self, offset: usize) -> Option<TextFormat> {
let inner = self.doc.lock();
let fragments = build_fragments(&inner, self.block_id as u64);
for frag in &fragments {
match frag {
FragmentContent::Text {
format,
offset: frag_offset,
length,
..
} => {
if offset >= *frag_offset && offset < frag_offset + length {
return Some(format.clone());
}
}
FragmentContent::Image {
format,
offset: frag_offset,
..
} => {
if offset == *frag_offset {
return Some(format.clone());
}
}
}
}
None
}
pub fn fragments(&self) -> Vec<FragmentContent> {
let inner = self.doc.lock();
build_fragments(&inner, self.block_id as u64)
}
pub fn list(&self) -> Option<TextList> {
let inner = self.doc.lock();
let block_dto = block_commands::get_block(&inner.ctx, &(self.block_id as u64))
.ok()
.flatten()?;
let list_id = block_dto.list?;
Some(TextList {
doc: Arc::clone(&self.doc),
list_id: list_id as usize,
})
}
pub fn list_item_index(&self) -> Option<usize> {
let inner = self.doc.lock();
let block_dto = block_commands::get_block(&inner.ctx, &(self.block_id as u64))
.ok()
.flatten()?;
let list_id = block_dto.list?;
Some(compute_list_item_index(
&inner,
list_id,
self.block_id as u64,
))
}
pub fn snapshot(&self) -> BlockSnapshot {
let inner = self.doc.lock();
build_block_snapshot(&inner, self.block_id as u64).unwrap_or_else(|| BlockSnapshot {
block_id: self.block_id,
position: 0,
length: 0,
text: String::new(),
fragments: Vec::new(),
block_format: BlockFormat::default(),
list_info: None,
parent_frame_id: None,
table_cell: None,
})
}
}
fn find_parent_frame(inner: &TextDocumentInner, block_id: u64) -> Option<EntityId> {
let all_frames = frame_commands::get_all_frame(&inner.ctx).ok()?;
let block_entity_id = block_id as EntityId;
for frame in &all_frames {
if frame.blocks.contains(&block_entity_id) {
return Some(frame.id as EntityId);
}
}
None
}
fn document_has_no_tables(inner: &TextDocumentInner) -> bool {
inner
.ctx
.db_context
.get_store()
.tables
.read()
.unwrap()
.is_empty()
}
fn find_table_cell_context(inner: &TextDocumentInner, block_id: u64) -> Option<TableCellContext> {
if document_has_no_tables(inner) {
return None;
}
let frame_id = find_parent_frame(inner, block_id)?;
let frame_dto = frame_commands::get_frame(&inner.ctx, &frame_id)
.ok()
.flatten()?;
if let Some(table_entity_id) = frame_dto.table {
let table_dto =
frontend::commands::table_commands::get_table(&inner.ctx, &{ table_entity_id })
.ok()
.flatten()?;
for &cell_id in &table_dto.cells {
if let Some(cell_dto) =
frontend::commands::table_cell_commands::get_table_cell(&inner.ctx, &{ cell_id })
.ok()
.flatten()
&& cell_dto.cell_frame == Some(frame_id)
{
return Some(TableCellContext {
table_id: table_entity_id as usize,
row: to_usize(cell_dto.row),
column: to_usize(cell_dto.column),
});
}
}
}
let all_tables =
frontend::commands::table_commands::get_all_table(&inner.ctx).unwrap_or_default();
for table_dto in &all_tables {
for &cell_id in &table_dto.cells {
if let Some(cell_dto) =
frontend::commands::table_cell_commands::get_table_cell(&inner.ctx, &{ cell_id })
.ok()
.flatten()
&& cell_dto.cell_frame == Some(frame_id)
{
return Some(TableCellContext {
table_id: table_dto.id as usize,
row: to_usize(cell_dto.row),
column: to_usize(cell_dto.column),
});
}
}
}
None
}
fn compute_block_number(inner: &TextDocumentInner, block_id: u64) -> usize {
let mut all_blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
let store = inner.ctx.db_context.get_store();
crate::inner::refresh_block_positions(&mut all_blocks, store);
let mut sorted: Vec<_> = all_blocks.iter().collect();
sorted.sort_by_key(|b| b.document_position);
sorted.iter().position(|b| b.id == block_id).unwrap_or(0)
}
pub(crate) fn build_fragments(inner: &TextDocumentInner, block_id: u64) -> Vec<FragmentContent> {
build_fragments_with_text(inner, block_id, None)
}
pub(crate) fn build_fragments_with_text(
inner: &TextDocumentInner,
block_id: u64,
prefetched_text: Option<&str>,
) -> Vec<FragmentContent> {
let fragments = build_raw_fragments(inner, block_id, prefetched_text);
if let Some(ref hl) = inner.highlight
&& let Some(block_hl) = hl.blocks.get(&(block_id as usize))
&& !block_hl.spans.is_empty()
{
return crate::highlight::merge_highlight_spans(fragments, &block_hl.spans);
}
fragments
}
fn build_raw_fragments(
inner: &TextDocumentInner,
block_id: u64,
prefetched_text: Option<&str>,
) -> Vec<FragmentContent> {
let _block_dto = match block_commands::get_block(&inner.ctx, &block_id)
.ok()
.flatten()
{
Some(b) => b,
None => return Vec::new(),
};
let plain_owned;
let plain: &str = match prefetched_text {
Some(t) => t,
None => {
let entity: common::entities::Block = _block_dto.clone().into();
plain_owned = common::database::rope_helpers::block_content_via_store(
&entity,
inner.ctx.db_context.get_store(),
);
&plain_owned
}
};
let (runs, images) = {
let store = inner.ctx.db_context.get_store();
let runs: Vec<FormatRun> = store
.format_runs
.read()
.unwrap()
.get(&block_id)
.cloned()
.unwrap_or_default();
let images: Vec<ImageAnchor> = store
.block_images
.read()
.unwrap()
.get(&block_id)
.cloned()
.unwrap_or_default();
(runs, images)
};
let mut fragments = Vec::with_capacity(runs.len() + images.len() + 1);
let mut char_offset: usize = 0;
let mut byte_cursor: u32 = 0;
let mut img_iter = images.iter().peekable();
fn emit_default_text(
fragments: &mut Vec<FragmentContent>,
plain: &str,
block_id: u64,
byte_a: u32,
byte_b: u32,
char_offset: &mut usize,
byte_cursor: &mut u32,
) {
if byte_a >= byte_b {
return;
}
let text = &plain[byte_a as usize..byte_b as usize];
let length = text.chars().count();
let word_starts = compute_word_starts(text);
fragments.push(FragmentContent::Text {
text: text.to_string(),
format: TextFormat::default(),
offset: *char_offset,
length,
element_id: synth_element_id(block_id, byte_a),
word_starts,
});
*char_offset += length;
*byte_cursor = byte_b;
}
#[allow(clippy::too_many_arguments)]
fn emit_run_text(
fragments: &mut Vec<FragmentContent>,
plain: &str,
block_id: u64,
byte_a: u32,
byte_b: u32,
run_format: &frontend::common::format_runs::CharacterFormat,
char_offset: &mut usize,
byte_cursor: &mut u32,
) {
if byte_a >= byte_b {
return;
}
let text = &plain[byte_a as usize..byte_b as usize];
let length = text.chars().count();
let word_starts = compute_word_starts(text);
fragments.push(FragmentContent::Text {
text: text.to_string(),
format: TextFormat::from(run_format),
offset: *char_offset,
length,
element_id: synth_element_id(block_id, byte_a),
word_starts,
});
*char_offset += length;
*byte_cursor = byte_b;
}
for run in &runs {
let mut run_cursor = run.byte_start;
while let Some(img) = img_iter.peek() {
if img.byte_offset < run.byte_start {
emit_default_text(
&mut fragments,
plain,
block_id,
byte_cursor,
img.byte_offset,
&mut char_offset,
&mut byte_cursor,
);
fragments.push(FragmentContent::Image {
name: img.name.clone(),
width: img.width as u32,
height: img.height as u32,
quality: img.quality as u32,
format: TextFormat::from(&img.format),
offset: char_offset,
element_id: synth_element_id(block_id, img.byte_offset),
});
char_offset += 1;
img_iter.next();
} else if img.byte_offset <= run.byte_end {
emit_default_text(
&mut fragments,
plain,
block_id,
byte_cursor,
run_cursor,
&mut char_offset,
&mut byte_cursor,
);
emit_run_text(
&mut fragments,
plain,
block_id,
run_cursor,
img.byte_offset,
&run.format,
&mut char_offset,
&mut byte_cursor,
);
fragments.push(FragmentContent::Image {
name: img.name.clone(),
width: img.width as u32,
height: img.height as u32,
quality: img.quality as u32,
format: TextFormat::from(&img.format),
offset: char_offset,
element_id: synth_element_id(block_id, img.byte_offset),
});
char_offset += 1;
run_cursor = img.byte_offset;
byte_cursor = img.byte_offset;
img_iter.next();
} else {
break;
}
}
emit_default_text(
&mut fragments,
plain,
block_id,
byte_cursor,
run_cursor,
&mut char_offset,
&mut byte_cursor,
);
emit_run_text(
&mut fragments,
plain,
block_id,
run_cursor,
run.byte_end,
&run.format,
&mut char_offset,
&mut byte_cursor,
);
}
for img in img_iter {
emit_default_text(
&mut fragments,
plain,
block_id,
byte_cursor,
img.byte_offset,
&mut char_offset,
&mut byte_cursor,
);
fragments.push(FragmentContent::Image {
name: img.name.clone(),
width: img.width as u32,
height: img.height as u32,
quality: img.quality as u32,
format: TextFormat::from(&img.format),
offset: char_offset,
element_id: synth_element_id(block_id, img.byte_offset),
});
char_offset += 1;
}
emit_default_text(
&mut fragments,
plain,
block_id,
byte_cursor,
plain.len() as u32,
&mut char_offset,
&mut byte_cursor,
);
fragments
}
fn compute_word_starts(text: &str) -> Vec<u8> {
use unicode_segmentation::UnicodeSegmentation;
let mut result = Vec::new();
let mut byte_to_char: Vec<(usize, usize)> = Vec::new();
for (ci, (bi, _)) in text.char_indices().enumerate() {
byte_to_char.push((bi, ci));
}
for (byte_off, _word) in text.unicode_word_indices() {
let char_idx = byte_to_char
.iter()
.find(|(bi, _)| *bi == byte_off)
.map(|(_, ci)| *ci)
.unwrap_or(0);
if let Ok(idx) = u8::try_from(char_idx) {
result.push(idx);
} else {
break;
}
}
result
}
fn compute_list_item_index(inner: &TextDocumentInner, list_id: EntityId, block_id: u64) -> usize {
let mut all_blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
let store = inner.ctx.db_context.get_store();
crate::inner::refresh_block_positions(&mut all_blocks, store);
let mut list_blocks: Vec<_> = all_blocks
.iter()
.filter(|b| b.list == Some(list_id))
.collect();
list_blocks.sort_by_key(|b| b.document_position);
list_blocks
.iter()
.position(|b| b.id == block_id)
.unwrap_or(0)
}
pub(crate) fn format_list_marker(
list_dto: &frontend::list::dtos::ListDto,
item_index: usize,
) -> String {
let number = item_index + 1; let marker_body = match list_dto.style {
ListStyle::Disc => "\u{2022}".to_string(), ListStyle::Circle => "\u{25E6}".to_string(), ListStyle::Square => "\u{25AA}".to_string(), ListStyle::Decimal => format!("{number}"),
ListStyle::LowerAlpha => {
if number <= 26 {
((b'a' + (number as u8 - 1)) as char).to_string()
} else {
format!("{number}")
}
}
ListStyle::UpperAlpha => {
if number <= 26 {
((b'A' + (number as u8 - 1)) as char).to_string()
} else {
format!("{number}")
}
}
ListStyle::LowerRoman => to_roman_lower(number),
ListStyle::UpperRoman => to_roman_upper(number),
};
format!("{}{marker_body}{}", list_dto.prefix, list_dto.suffix)
}
fn to_roman_upper(mut n: usize) -> String {
const VALUES: &[(usize, &str)] = &[
(1000, "M"),
(900, "CM"),
(500, "D"),
(400, "CD"),
(100, "C"),
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I"),
];
let mut result = String::new();
for &(val, sym) in VALUES {
while n >= val {
result.push_str(sym);
n -= val;
}
}
result
}
fn to_roman_lower(n: usize) -> String {
to_roman_upper(n).to_lowercase()
}
fn build_list_info(
inner: &TextDocumentInner,
block_dto: &frontend::block::dtos::BlockDto,
) -> Option<ListInfo> {
let list_id = block_dto.list?;
let list_dto = list_commands::get_list(&inner.ctx, &{ list_id })
.ok()
.flatten()?;
let item_index = compute_list_item_index(inner, list_id, block_dto.id);
let marker = format_list_marker(&list_dto, item_index);
Some(ListInfo {
list_id: list_id as usize,
style: list_dto.style.clone(),
indent: list_dto.indent as u8,
marker,
item_index,
})
}
pub(crate) fn build_block_snapshot(
inner: &TextDocumentInner,
block_id: u64,
) -> Option<BlockSnapshot> {
build_block_snapshot_with_position_and_parent(inner, block_id, None, None)
}
pub(crate) fn build_block_snapshot_with_position(
inner: &TextDocumentInner,
block_id: u64,
computed_position: Option<usize>,
) -> Option<BlockSnapshot> {
build_block_snapshot_with_position_and_parent(inner, block_id, computed_position, None)
}
pub(crate) fn build_block_snapshot_with_position_and_parent(
inner: &TextDocumentInner,
block_id: u64,
computed_position: Option<usize>,
parent_frame_hint: Option<EntityId>,
) -> Option<BlockSnapshot> {
let mut block_dto = block_commands::get_block(&inner.ctx, &block_id)
.ok()
.flatten()?;
let store_for_pos = inner.ctx.db_context.get_store();
crate::inner::refresh_block_position(&mut block_dto, store_for_pos);
let block_format = BlockFormat::from(&block_dto);
let list_info = build_list_info(inner, &block_dto);
let parent_frame_id = parent_frame_hint
.or_else(|| find_parent_frame(inner, block_id))
.map(|id| id as usize);
let table_cell = find_table_cell_context(inner, block_id);
let position = computed_position.unwrap_or_else(|| to_usize(block_dto.document_position));
let entity: common::entities::Block = block_dto.clone().into();
let store = inner.ctx.db_context.get_store();
let text = common::database::rope_helpers::block_content_via_store(&entity, store);
let length = to_usize(common::database::rope_helpers::block_char_length(
&entity, store,
));
let fragments = build_fragments_with_text(inner, block_id, Some(&text));
Some(BlockSnapshot {
block_id: block_id as usize,
position,
length,
text,
fragments,
block_format,
list_info,
parent_frame_id,
table_cell,
})
}
pub(crate) fn build_blocks_snapshot_for_frame(
inner: &TextDocumentInner,
frame_id: u64,
) -> Vec<BlockSnapshot> {
let frame_dto = match frame_commands::get_frame(&inner.ctx, &(frame_id as EntityId))
.ok()
.flatten()
{
Some(f) => f,
None => return Vec::new(),
};
let mut block_dtos: Vec<_> = frame_dto
.blocks
.iter()
.filter_map(|&id| {
block_commands::get_block(&inner.ctx, &{ id })
.ok()
.flatten()
})
.collect();
let store = inner.ctx.db_context.get_store();
crate::inner::refresh_block_positions(&mut block_dtos, store);
block_dtos.sort_by_key(|b| b.document_position);
block_dtos
.iter()
.filter_map(|b| build_block_snapshot(inner, b.id))
.collect()
}
pub(crate) fn build_blocks_snapshot_for_frame_with_positions(
inner: &TextDocumentInner,
frame_id: u64,
start_pos: usize,
) -> (Vec<BlockSnapshot>, usize) {
let frame_dto = match frame_commands::get_frame(&inner.ctx, &(frame_id as EntityId))
.ok()
.flatten()
{
Some(f) => f,
None => return (Vec::new(), start_pos),
};
let mut block_dtos: Vec<_> = frame_dto
.blocks
.iter()
.filter_map(|&id| {
block_commands::get_block(&inner.ctx, &{ id })
.ok()
.flatten()
})
.collect();
let store = inner.ctx.db_context.get_store();
crate::inner::refresh_block_positions(&mut block_dtos, store);
block_dtos.sort_by_key(|b| b.document_position);
let mut running_pos = start_pos;
let mut snapshots = Vec::with_capacity(block_dtos.len());
for b in &block_dtos {
if let Some(snap) = build_block_snapshot_with_position(inner, b.id, Some(running_pos)) {
running_pos += snap.length + 1; snapshots.push(snap);
}
}
(snapshots, running_pos)
}