Skip to main content

text_document/
text_block.rs

1//! Read-only block (paragraph) handle.
2
3use std::sync::Arc;
4
5use parking_lot::Mutex;
6
7use frontend::commands::{block_commands, frame_commands, inline_element_commands, list_commands};
8use frontend::common::types::EntityId;
9use frontend::inline_element::dtos::InlineContent;
10
11use crate::convert::to_usize;
12use crate::flow::{BlockSnapshot, FragmentContent, ListInfo, TableCellContext, TableCellRef};
13use crate::inner::TextDocumentInner;
14use crate::text_frame::TextFrame;
15use crate::text_list::TextList;
16use crate::text_table::TextTable;
17use crate::{BlockFormat, ListStyle, TextFormat};
18
19/// A lightweight, read-only handle to a single block (paragraph).
20///
21/// Holds a stable entity ID — the handle remains valid across edits
22/// that insert or remove other blocks. Each method acquires the
23/// document lock independently. For consistent reads across multiple
24/// fields, use [`snapshot()`](TextBlock::snapshot).
25#[derive(Clone)]
26pub struct TextBlock {
27    pub(crate) doc: Arc<Mutex<TextDocumentInner>>,
28    pub(crate) block_id: usize,
29}
30
31impl TextBlock {
32    // ── Content ──────────────────────────────────────────────
33
34    /// Block's plain text. O(1).
35    pub fn text(&self) -> String {
36        let inner = self.doc.lock();
37        block_commands::get_block(&inner.ctx, &(self.block_id as u64))
38            .ok()
39            .flatten()
40            .map(|b| b.plain_text)
41            .unwrap_or_default()
42    }
43
44    /// Character count. O(1).
45    pub fn length(&self) -> usize {
46        let inner = self.doc.lock();
47        block_commands::get_block(&inner.ctx, &(self.block_id as u64))
48            .ok()
49            .flatten()
50            .map(|b| to_usize(b.text_length))
51            .unwrap_or(0)
52    }
53
54    /// `length() == 0`. O(1).
55    pub fn is_empty(&self) -> bool {
56        let inner = self.doc.lock();
57        block_commands::get_block(&inner.ctx, &(self.block_id as u64))
58            .ok()
59            .flatten()
60            .map(|b| b.text_length == 0)
61            .unwrap_or(true)
62    }
63
64    /// Block entity still exists in the database. O(1).
65    pub fn is_valid(&self) -> bool {
66        let inner = self.doc.lock();
67        block_commands::get_block(&inner.ctx, &(self.block_id as u64))
68            .ok()
69            .flatten()
70            .is_some()
71    }
72
73    // ── Identity and Position ────────────────────────────────
74
75    /// Stable entity ID (stored in the handle). O(1).
76    pub fn id(&self) -> usize {
77        self.block_id
78    }
79
80    /// Character offset from `Block.document_position`. O(1).
81    pub fn position(&self) -> usize {
82        let inner = self.doc.lock();
83        block_commands::get_block(&inner.ctx, &(self.block_id as u64))
84            .ok()
85            .flatten()
86            .map(|b| to_usize(b.document_position))
87            .unwrap_or(0)
88    }
89
90    /// Global 0-indexed block number. **O(n)**: requires scanning all blocks
91    /// sorted by `document_position`. Prefer [`id()`](TextBlock::id) for
92    /// identity and [`position()`](TextBlock::position) for ordering.
93    pub fn block_number(&self) -> usize {
94        let inner = self.doc.lock();
95        compute_block_number(&inner, self.block_id as u64)
96    }
97
98    /// The next block in document order. **O(n)**.
99    /// Returns `None` if this is the last block.
100    pub fn next(&self) -> Option<TextBlock> {
101        let inner = self.doc.lock();
102        let all_blocks = block_commands::get_all_block(&inner.ctx).ok()?;
103        let mut sorted: Vec<_> = all_blocks.into_iter().collect();
104        sorted.sort_by_key(|b| b.document_position);
105        let idx = sorted.iter().position(|b| b.id == self.block_id as u64)?;
106        sorted.get(idx + 1).map(|b| TextBlock {
107            doc: Arc::clone(&self.doc),
108            block_id: b.id as usize,
109        })
110    }
111
112    /// The previous block in document order. **O(n)**.
113    /// Returns `None` if this is the first block.
114    pub fn previous(&self) -> Option<TextBlock> {
115        let inner = self.doc.lock();
116        let all_blocks = block_commands::get_all_block(&inner.ctx).ok()?;
117        let mut sorted: Vec<_> = all_blocks.into_iter().collect();
118        sorted.sort_by_key(|b| b.document_position);
119        let idx = sorted.iter().position(|b| b.id == self.block_id as u64)?;
120        if idx == 0 {
121            return None;
122        }
123        sorted.get(idx - 1).map(|b| TextBlock {
124            doc: Arc::clone(&self.doc),
125            block_id: b.id as usize,
126        })
127    }
128
129    // ── Structural Context ───────────────────────────────────
130
131    /// Parent frame. O(1).
132    pub fn frame(&self) -> TextFrame {
133        let inner = self.doc.lock();
134        let frame_id = find_parent_frame(&inner, self.block_id as u64);
135        TextFrame {
136            doc: Arc::clone(&self.doc),
137            frame_id: frame_id.map(|id| id as usize).unwrap_or(0),
138        }
139    }
140
141    /// If inside a table cell, returns table and cell coordinates.
142    ///
143    /// Finds the block's parent frame, then checks if any table cell
144    /// references that frame as its `cell_frame`. If so, identifies the
145    /// owning table.
146    pub fn table_cell(&self) -> Option<TableCellRef> {
147        let inner = self.doc.lock();
148        let frame_id = find_parent_frame(&inner, self.block_id as u64)?;
149
150        // Check if this frame is referenced as a cell_frame by any table cell.
151        // First try the fast path: if the frame has a `table` field, use it.
152        let frame_dto = frame_commands::get_frame(&inner.ctx, &frame_id)
153            .ok()
154            .flatten()?;
155
156        if let Some(table_entity_id) = frame_dto.table {
157            // This frame is a table anchor frame (not a cell frame).
158            // Anchor frames don't contain blocks directly — cell frames do.
159            // So this path shouldn't match, but check cells just in case.
160            let table_dto =
161                frontend::commands::table_commands::get_table(&inner.ctx, &{ table_entity_id })
162                    .ok()
163                    .flatten()?;
164            for &cell_id in &table_dto.cells {
165                if let Some(cell_dto) =
166                    frontend::commands::table_cell_commands::get_table_cell(&inner.ctx, &{
167                        cell_id
168                    })
169                    .ok()
170                    .flatten()
171                    && cell_dto.cell_frame == Some(frame_id)
172                {
173                    return Some(TableCellRef {
174                        table: TextTable {
175                            doc: Arc::clone(&self.doc),
176                            table_id: table_entity_id as usize,
177                        },
178                        row: to_usize(cell_dto.row),
179                        column: to_usize(cell_dto.column),
180                    });
181                }
182            }
183        }
184
185        // Slow path: this frame has no `table` field (cell frames don't).
186        // Scan all tables to find if any cell references this frame.
187        let all_tables =
188            frontend::commands::table_commands::get_all_table(&inner.ctx).unwrap_or_default();
189        for table_dto in &all_tables {
190            for &cell_id in &table_dto.cells {
191                if let Some(cell_dto) =
192                    frontend::commands::table_cell_commands::get_table_cell(&inner.ctx, &{
193                        cell_id
194                    })
195                    .ok()
196                    .flatten()
197                    && cell_dto.cell_frame == Some(frame_id)
198                {
199                    return Some(TableCellRef {
200                        table: TextTable {
201                            doc: Arc::clone(&self.doc),
202                            table_id: table_dto.id as usize,
203                        },
204                        row: to_usize(cell_dto.row),
205                        column: to_usize(cell_dto.column),
206                    });
207                }
208            }
209        }
210
211        None
212    }
213
214    // ── Formatting ──────────────────────────────────────────
215
216    /// Block format (alignment, margins, indent, heading level, marker, tabs). O(1).
217    pub fn block_format(&self) -> BlockFormat {
218        let inner = self.doc.lock();
219        block_commands::get_block(&inner.ctx, &(self.block_id as u64))
220            .ok()
221            .flatten()
222            .map(|b| BlockFormat::from(&b))
223            .unwrap_or_default()
224    }
225
226    /// Character format at a block-relative character offset. **O(k)**
227    /// where k = number of InlineElements.
228    ///
229    /// Returns the [`TextFormat`] of the fragment containing the given
230    /// offset. Returns `None` if the offset is out of range or the
231    /// block has no fragments.
232    pub fn char_format_at(&self, offset: usize) -> Option<TextFormat> {
233        let inner = self.doc.lock();
234        let fragments = build_fragments(&inner, self.block_id as u64);
235        for frag in &fragments {
236            match frag {
237                FragmentContent::Text {
238                    format,
239                    offset: frag_offset,
240                    length,
241                    ..
242                } => {
243                    if offset >= *frag_offset && offset < frag_offset + length {
244                        return Some(format.clone());
245                    }
246                }
247                FragmentContent::Image {
248                    format,
249                    offset: frag_offset,
250                    ..
251                } => {
252                    if offset == *frag_offset {
253                        return Some(format.clone());
254                    }
255                }
256            }
257        }
258        None
259    }
260
261    // ── Fragments ───────────────────────────────────────────
262
263    /// All formatting runs in one call. O(k) where k = number of InlineElements.
264    pub fn fragments(&self) -> Vec<FragmentContent> {
265        let inner = self.doc.lock();
266        build_fragments(&inner, self.block_id as u64)
267    }
268
269    // ── List Membership ─────────────────────────────────────
270
271    /// List this block belongs to. O(1).
272    pub fn list(&self) -> Option<TextList> {
273        let inner = self.doc.lock();
274        let block_dto = block_commands::get_block(&inner.ctx, &(self.block_id as u64))
275            .ok()
276            .flatten()?;
277        let list_id = block_dto.list?;
278        Some(TextList {
279            doc: Arc::clone(&self.doc),
280            list_id: list_id as usize,
281        })
282    }
283
284    /// 0-based position within its list. **O(n)** where n = total blocks.
285    pub fn list_item_index(&self) -> Option<usize> {
286        let inner = self.doc.lock();
287        let block_dto = block_commands::get_block(&inner.ctx, &(self.block_id as u64))
288            .ok()
289            .flatten()?;
290        let list_id = block_dto.list?;
291        Some(compute_list_item_index(
292            &inner,
293            list_id,
294            self.block_id as u64,
295        ))
296    }
297
298    // ── Snapshot ─────────────────────────────────────────────
299
300    /// All layout-relevant data in one lock acquisition. O(k+n).
301    pub fn snapshot(&self) -> BlockSnapshot {
302        let inner = self.doc.lock();
303        build_block_snapshot(&inner, self.block_id as u64).unwrap_or_else(|| BlockSnapshot {
304            block_id: self.block_id,
305            position: 0,
306            length: 0,
307            text: String::new(),
308            fragments: Vec::new(),
309            block_format: BlockFormat::default(),
310            list_info: None,
311            parent_frame_id: None,
312            table_cell: None,
313        })
314    }
315}
316
317// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
318// Internal helpers (called while lock is held)
319// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
320
321/// Find the parent frame of a block by scanning all frames.
322fn find_parent_frame(inner: &TextDocumentInner, block_id: u64) -> Option<EntityId> {
323    let all_frames = frame_commands::get_all_frame(&inner.ctx).ok()?;
324    let block_entity_id = block_id as EntityId;
325    for frame in &all_frames {
326        if frame.blocks.contains(&block_entity_id) {
327            return Some(frame.id as EntityId);
328        }
329    }
330    None
331}
332
333/// Find table cell context for a block (snapshot-friendly, no live handles).
334/// Returns `None` if the block is not inside a table cell.
335fn find_table_cell_context(inner: &TextDocumentInner, block_id: u64) -> Option<TableCellContext> {
336    let frame_id = find_parent_frame(inner, block_id)?;
337
338    let frame_dto = frame_commands::get_frame(&inner.ctx, &frame_id)
339        .ok()
340        .flatten()?;
341
342    // Fast path: anchor frame with `table` field set
343    if let Some(table_entity_id) = frame_dto.table {
344        let table_dto =
345            frontend::commands::table_commands::get_table(&inner.ctx, &{ table_entity_id })
346                .ok()
347                .flatten()?;
348        for &cell_id in &table_dto.cells {
349            if let Some(cell_dto) =
350                frontend::commands::table_cell_commands::get_table_cell(&inner.ctx, &{ cell_id })
351                    .ok()
352                    .flatten()
353                && cell_dto.cell_frame == Some(frame_id)
354            {
355                return Some(TableCellContext {
356                    table_id: table_entity_id as usize,
357                    row: to_usize(cell_dto.row),
358                    column: to_usize(cell_dto.column),
359                });
360            }
361        }
362    }
363
364    // Slow path: scan all tables for a cell referencing this frame
365    let all_tables =
366        frontend::commands::table_commands::get_all_table(&inner.ctx).unwrap_or_default();
367    for table_dto in &all_tables {
368        for &cell_id in &table_dto.cells {
369            if let Some(cell_dto) =
370                frontend::commands::table_cell_commands::get_table_cell(&inner.ctx, &{ cell_id })
371                    .ok()
372                    .flatten()
373                && cell_dto.cell_frame == Some(frame_id)
374            {
375                return Some(TableCellContext {
376                    table_id: table_dto.id as usize,
377                    row: to_usize(cell_dto.row),
378                    column: to_usize(cell_dto.column),
379                });
380            }
381        }
382    }
383
384    None
385}
386
387/// Compute 0-indexed block number by scanning all blocks sorted by document_position.
388fn compute_block_number(inner: &TextDocumentInner, block_id: u64) -> usize {
389    let all_blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
390    let mut sorted: Vec<_> = all_blocks.iter().collect();
391    sorted.sort_by_key(|b| b.document_position);
392    sorted.iter().position(|b| b.id == block_id).unwrap_or(0)
393}
394
395/// Build fragments for a block from its InlineElements, with highlight
396/// spans merged in when a syntax highlighter is attached.
397pub(crate) fn build_fragments(inner: &TextDocumentInner, block_id: u64) -> Vec<FragmentContent> {
398    let fragments = build_raw_fragments(inner, block_id);
399
400    if let Some(ref hl) = inner.highlight
401        && let Some(block_hl) = hl.blocks.get(&(block_id as usize))
402        && !block_hl.spans.is_empty()
403    {
404        return crate::highlight::merge_highlight_spans(fragments, &block_hl.spans);
405    }
406
407    fragments
408}
409
410/// Build raw fragments from InlineElements (no highlight merge).
411fn build_raw_fragments(inner: &TextDocumentInner, block_id: u64) -> Vec<FragmentContent> {
412    let block_dto = match block_commands::get_block(&inner.ctx, &block_id)
413        .ok()
414        .flatten()
415    {
416        Some(b) => b,
417        None => return Vec::new(),
418    };
419
420    let element_ids = &block_dto.elements;
421    let elements: Vec<_> = element_ids
422        .iter()
423        .filter_map(|&id| {
424            inline_element_commands::get_inline_element(&inner.ctx, &{ id })
425                .ok()
426                .flatten()
427        })
428        .collect();
429
430    let mut fragments = Vec::with_capacity(elements.len());
431    let mut offset: usize = 0;
432
433    for el in &elements {
434        let format = TextFormat::from(el);
435        match &el.content {
436            InlineContent::Text(text) => {
437                let length = text.chars().count();
438                fragments.push(FragmentContent::Text {
439                    text: text.clone(),
440                    format,
441                    offset,
442                    length,
443                });
444                offset += length;
445            }
446            InlineContent::Image {
447                name,
448                width,
449                height,
450                quality,
451            } => {
452                fragments.push(FragmentContent::Image {
453                    name: name.clone(),
454                    width: *width as u32,
455                    height: *height as u32,
456                    quality: *quality as u32,
457                    format,
458                    offset,
459                });
460                offset += 1; // images take 1 character position
461            }
462            InlineContent::Empty => {
463                // Empty elements don't produce fragments
464            }
465        }
466    }
467
468    fragments
469}
470
471/// Compute 0-based index of a block within its list.
472fn compute_list_item_index(inner: &TextDocumentInner, list_id: EntityId, block_id: u64) -> usize {
473    let all_blocks = block_commands::get_all_block(&inner.ctx).unwrap_or_default();
474    let mut list_blocks: Vec<_> = all_blocks
475        .iter()
476        .filter(|b| b.list == Some(list_id))
477        .collect();
478    list_blocks.sort_by_key(|b| b.document_position);
479    list_blocks
480        .iter()
481        .position(|b| b.id == block_id)
482        .unwrap_or(0)
483}
484
485/// Format a list marker for the given item index.
486pub(crate) fn format_list_marker(
487    list_dto: &frontend::list::dtos::ListDto,
488    item_index: usize,
489) -> String {
490    let number = item_index + 1; // 1-based for display
491    let marker_body = match list_dto.style {
492        ListStyle::Disc => "\u{2022}".to_string(),   // •
493        ListStyle::Circle => "\u{25E6}".to_string(), // ◦
494        ListStyle::Square => "\u{25AA}".to_string(), // ▪
495        ListStyle::Decimal => format!("{number}"),
496        ListStyle::LowerAlpha => {
497            if number <= 26 {
498                ((b'a' + (number as u8 - 1)) as char).to_string()
499            } else {
500                format!("{number}")
501            }
502        }
503        ListStyle::UpperAlpha => {
504            if number <= 26 {
505                ((b'A' + (number as u8 - 1)) as char).to_string()
506            } else {
507                format!("{number}")
508            }
509        }
510        ListStyle::LowerRoman => to_roman_lower(number),
511        ListStyle::UpperRoman => to_roman_upper(number),
512    };
513    format!("{}{marker_body}{}", list_dto.prefix, list_dto.suffix)
514}
515
516fn to_roman_upper(mut n: usize) -> String {
517    const VALUES: &[(usize, &str)] = &[
518        (1000, "M"),
519        (900, "CM"),
520        (500, "D"),
521        (400, "CD"),
522        (100, "C"),
523        (90, "XC"),
524        (50, "L"),
525        (40, "XL"),
526        (10, "X"),
527        (9, "IX"),
528        (5, "V"),
529        (4, "IV"),
530        (1, "I"),
531    ];
532    let mut result = String::new();
533    for &(val, sym) in VALUES {
534        while n >= val {
535            result.push_str(sym);
536            n -= val;
537        }
538    }
539    result
540}
541
542fn to_roman_lower(n: usize) -> String {
543    to_roman_upper(n).to_lowercase()
544}
545
546/// Build a ListInfo for a block. Called while lock is held.
547fn build_list_info(
548    inner: &TextDocumentInner,
549    block_dto: &frontend::block::dtos::BlockDto,
550) -> Option<ListInfo> {
551    let list_id = block_dto.list?;
552    let list_dto = list_commands::get_list(&inner.ctx, &{ list_id })
553        .ok()
554        .flatten()?;
555
556    let item_index = compute_list_item_index(inner, list_id, block_dto.id);
557    let marker = format_list_marker(&list_dto, item_index);
558
559    Some(ListInfo {
560        list_id: list_id as usize,
561        style: list_dto.style.clone(),
562        indent: list_dto.indent as u8,
563        marker,
564        item_index,
565    })
566}
567
568/// Build a BlockSnapshot for a block. Called while lock is held.
569pub(crate) fn build_block_snapshot(
570    inner: &TextDocumentInner,
571    block_id: u64,
572) -> Option<BlockSnapshot> {
573    let block_dto = block_commands::get_block(&inner.ctx, &block_id)
574        .ok()
575        .flatten()?;
576
577    let fragments = build_fragments(inner, block_id);
578    let block_format = BlockFormat::from(&block_dto);
579    let list_info = build_list_info(inner, &block_dto);
580
581    let parent_frame_id = find_parent_frame(inner, block_id).map(|id| id as usize);
582    let table_cell = find_table_cell_context(inner, block_id);
583
584    Some(BlockSnapshot {
585        block_id: block_id as usize,
586        position: to_usize(block_dto.document_position),
587        length: to_usize(block_dto.text_length),
588        text: block_dto.plain_text,
589        fragments,
590        block_format,
591        list_info,
592        parent_frame_id,
593        table_cell,
594    })
595}
596
597/// Build BlockSnapshots for all blocks in a frame, sorted by document_position.
598pub(crate) fn build_blocks_snapshot_for_frame(
599    inner: &TextDocumentInner,
600    frame_id: u64,
601) -> Vec<BlockSnapshot> {
602    let frame_dto = match frame_commands::get_frame(&inner.ctx, &(frame_id as EntityId))
603        .ok()
604        .flatten()
605    {
606        Some(f) => f,
607        None => return Vec::new(),
608    };
609
610    let mut block_dtos: Vec<_> = frame_dto
611        .blocks
612        .iter()
613        .filter_map(|&id| {
614            block_commands::get_block(&inner.ctx, &{ id })
615                .ok()
616                .flatten()
617        })
618        .collect();
619    block_dtos.sort_by_key(|b| b.document_position);
620
621    block_dtos
622        .iter()
623        .filter_map(|b| build_block_snapshot(inner, b.id))
624        .collect()
625}