Skip to main content

text_document/
text_frame.rs

1//! Read-only frame handle and shared flow traversal logic.
2
3use std::collections::HashSet;
4use std::sync::Arc;
5
6use parking_lot::Mutex;
7
8use frontend::commands::{block_commands, frame_commands, table_cell_commands, table_commands};
9use frontend::common::types::EntityId;
10
11use crate::FrameFormat;
12use crate::convert::to_usize;
13use crate::flow::{CellSnapshot, FlowElement, FlowElementSnapshot, FrameSnapshot, TableSnapshot};
14use crate::inner::TextDocumentInner;
15use crate::text_block::TextBlock;
16use crate::text_table::TextTable;
17
18/// A read-only handle to a frame in the document.
19///
20/// Obtained from [`FlowElement::Frame`] or [`TextBlock::frame()`].
21#[derive(Clone)]
22pub struct TextFrame {
23    pub(crate) doc: Arc<Mutex<TextDocumentInner>>,
24    pub(crate) frame_id: usize,
25}
26
27impl TextFrame {
28    /// Stable entity ID.
29    pub fn id(&self) -> usize {
30        self.frame_id
31    }
32
33    /// Frame formatting (height, width, margins, padding, border, position).
34    pub fn format(&self) -> FrameFormat {
35        let inner = self.doc.lock();
36        let frame_dto = frame_commands::get_frame(&inner.ctx, &(self.frame_id as EntityId))
37            .ok()
38            .flatten();
39        match frame_dto {
40            Some(f) => frame_dto_to_format(&f),
41            None => FrameFormat::default(),
42        }
43    }
44
45    /// Nested flow within this frame. Same `child_order` traversal as
46    /// [`TextDocument::flow()`](crate::TextDocument::flow).
47    pub fn flow(&self) -> Vec<FlowElement> {
48        let inner = self.doc.lock();
49        build_flow_elements(&inner, &self.doc, self.frame_id as EntityId)
50    }
51
52    /// Snapshot of this frame and all its contents, captured in a single
53    /// lock acquisition. Thread-safe — the returned [`FrameSnapshot`]
54    /// contains only plain data.
55    pub fn snapshot(&self) -> FrameSnapshot {
56        let inner = self.doc.lock();
57        let format = frame_commands::get_frame(&inner.ctx, &(self.frame_id as EntityId))
58            .ok()
59            .flatten()
60            .map(|f| frame_dto_to_format(&f))
61            .unwrap_or_default();
62        let elements = build_flow_snapshot(&inner, self.frame_id as EntityId, inner.highlight_kind);
63        FrameSnapshot {
64            frame_id: self.frame_id,
65            format,
66            elements,
67        }
68    }
69}
70
71// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
72// Shared flow traversal (used by TextDocument and TextFrame)
73// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
74
75/// Build flow elements for a frame, returning FlowElement variants.
76///
77/// This is the main entry point. `doc_arc` is the shared document handle
78/// that will be cloned into each returned handle.
79pub(crate) fn build_flow_elements(
80    inner: &TextDocumentInner,
81    doc_arc: &Arc<Mutex<TextDocumentInner>>,
82    frame_id: EntityId,
83) -> Vec<FlowElement> {
84    let frame_dto = match frame_commands::get_frame(&inner.ctx, &frame_id)
85        .ok()
86        .flatten()
87    {
88        Some(f) => f,
89        None => return Vec::new(),
90    };
91
92    if !frame_dto.child_order.is_empty() {
93        flow_from_child_order(inner, doc_arc, &frame_dto.child_order)
94    } else {
95        flow_fallback(inner, doc_arc, &frame_dto)
96    }
97}
98
99/// Build flow from populated `child_order`.
100fn flow_from_child_order(
101    inner: &TextDocumentInner,
102    doc_arc: &Arc<Mutex<TextDocumentInner>>,
103    child_order: &[i64],
104) -> Vec<FlowElement> {
105    let mut elements = Vec::with_capacity(child_order.len());
106
107    for &entry in child_order {
108        if entry > 0 {
109            // Positive: block ID
110            elements.push(FlowElement::Block(TextBlock {
111                doc: Arc::clone(doc_arc),
112                block_id: entry as usize,
113            }));
114        } else if entry < 0 {
115            // Negative: frame ID (negated)
116            let sub_frame_id = (-entry) as EntityId;
117            if let Some(sub_frame) = frame_commands::get_frame(&inner.ctx, &sub_frame_id)
118                .ok()
119                .flatten()
120            {
121                if let Some(table_id) = sub_frame.table {
122                    // Anchor frame for a table
123                    elements.push(FlowElement::Table(TextTable {
124                        doc: Arc::clone(doc_arc),
125                        table_id: table_id as usize,
126                    }));
127                } else {
128                    // Non-table sub-frame
129                    elements.push(FlowElement::Frame(TextFrame {
130                        doc: Arc::clone(doc_arc),
131                        frame_id: sub_frame_id as usize,
132                    }));
133                }
134            }
135        }
136        // entry == 0 is ignored (shouldn't happen)
137    }
138
139    elements
140}
141
142/// Fallback flow: iterate blocks sorted by document_position, skip cell frames.
143fn flow_fallback(
144    inner: &TextDocumentInner,
145    doc_arc: &Arc<Mutex<TextDocumentInner>>,
146    frame_dto: &frontend::frame::dtos::FrameDto,
147) -> Vec<FlowElement> {
148    // Build set of cell frame IDs to skip
149    let cell_frame_ids = build_cell_frame_ids(inner);
150
151    // Get blocks in this frame, sorted by document_position
152    let block_ids = &frame_dto.blocks;
153    let mut block_dtos: Vec<_> = block_ids
154        .iter()
155        .filter_map(|&id| {
156            block_commands::get_block(&inner.ctx, &{ id })
157                .ok()
158                .flatten()
159        })
160        .collect();
161    let store = inner.ctx.db_context.get_store();
162    crate::inner::refresh_block_positions(&mut block_dtos, store);
163    block_dtos.sort_by_key(|b| b.document_position);
164
165    let mut elements: Vec<FlowElement> = block_dtos
166        .iter()
167        .map(|b| {
168            FlowElement::Block(TextBlock {
169                doc: Arc::clone(doc_arc),
170                block_id: b.id as usize,
171            })
172        })
173        .collect();
174
175    // Also check for sub-frames that are children of this frame's document
176    // but not cell frames. In fallback mode, we can't interleave perfectly,
177    // so we append sub-frames after blocks.
178    // For the main frame, get all document frames and check parentage.
179    let all_frames = frame_commands::get_all_frame(&inner.ctx).unwrap_or_default();
180    for f in &all_frames {
181        if f.id == frame_dto.id {
182            continue; // skip self
183        }
184        if cell_frame_ids.contains(&(f.id as EntityId)) {
185            continue; // skip cell frames
186        }
187        // Check if this frame's parent is the current frame
188        if f.parent_frame == Some(frame_dto.id) {
189            if let Some(table_id) = f.table {
190                elements.push(FlowElement::Table(TextTable {
191                    doc: Arc::clone(doc_arc),
192                    table_id: table_id as usize,
193                }));
194            } else {
195                elements.push(FlowElement::Frame(TextFrame {
196                    doc: Arc::clone(doc_arc),
197                    frame_id: f.id as usize,
198                }));
199            }
200        }
201    }
202
203    elements
204}
205
206/// Build a set of all frame IDs that are table cell frames.
207fn build_cell_frame_ids(inner: &TextDocumentInner) -> HashSet<EntityId> {
208    let mut ids = HashSet::new();
209    let all_cells = table_cell_commands::get_all_table_cell(&inner.ctx).unwrap_or_default();
210    for cell in &all_cells {
211        if let Some(frame_id) = cell.cell_frame {
212            ids.insert(frame_id);
213        }
214    }
215    ids
216}
217
218// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
219// Snapshot helpers (called while lock is held)
220// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
221
222/// Build a FlowSnapshot for the given frame. Called while lock is held.
223///
224/// Block positions are computed on-the-fly from child_order + text_length
225/// rather than using stored `document_position` values (which may be stale
226/// after insert_text defers position updates for performance).
227pub(crate) fn build_flow_snapshot(
228    inner: &TextDocumentInner,
229    frame_id: EntityId,
230    effective_kind: crate::highlight::HighlighterKind,
231) -> Vec<FlowElementSnapshot> {
232    let frame_dto = match frame_commands::get_frame(&inner.ctx, &frame_id)
233        .ok()
234        .flatten()
235    {
236        Some(f) => f,
237        None => return Vec::new(),
238    };
239
240    if !frame_dto.child_order.is_empty() {
241        let (elements, _) =
242            snapshot_from_child_order(inner, &frame_dto.child_order, 0, frame_id, effective_kind);
243        elements
244    } else {
245        snapshot_fallback(inner, &frame_dto, effective_kind)
246    }
247}
248
249/// Walk child_order, building snapshots with on-the-fly position computation.
250/// `parent_frame_id` is passed down so per-block snapshots skip the
251/// expensive `find_parent_frame` walk over every frame in the store —
252/// a major contributor to per-keystroke editor lag.
253/// Returns (elements, running_position_after_last_block).
254fn snapshot_from_child_order(
255    inner: &TextDocumentInner,
256    child_order: &[i64],
257    start_pos: usize,
258    parent_frame_id: EntityId,
259    effective_kind: crate::highlight::HighlighterKind,
260) -> (Vec<FlowElementSnapshot>, usize) {
261    let mut elements = Vec::with_capacity(child_order.len());
262    let mut running_pos = start_pos;
263
264    for &entry in child_order {
265        if entry > 0 {
266            let block_id = entry as u64;
267            if let Some(snap) = crate::text_block::build_block_snapshot_with_position_and_parent(
268                inner,
269                block_id,
270                Some(running_pos),
271                Some(parent_frame_id),
272                effective_kind,
273            ) {
274                running_pos += snap.length + 1; // +1 for block separator
275                elements.push(FlowElementSnapshot::Block(snap));
276            }
277        } else if entry < 0 {
278            let sub_frame_id = (-entry) as EntityId;
279            if let Some(sub_frame) = frame_commands::get_frame(&inner.ctx, &sub_frame_id)
280                .ok()
281                .flatten()
282            {
283                if let Some(table_id) = sub_frame.table {
284                    if let Some((snap, new_pos)) = build_table_snapshot_with_positions(
285                        inner,
286                        table_id,
287                        running_pos,
288                        effective_kind,
289                    ) {
290                        running_pos = new_pos;
291                        elements.push(FlowElementSnapshot::Table(snap));
292                    }
293                } else {
294                    let (nested, new_pos) = snapshot_from_child_order(
295                        inner,
296                        &sub_frame.child_order,
297                        running_pos,
298                        sub_frame_id,
299                        effective_kind,
300                    );
301                    running_pos = new_pos;
302                    elements.push(FlowElementSnapshot::Frame(FrameSnapshot {
303                        frame_id: sub_frame_id as usize,
304                        format: frame_dto_to_format(&sub_frame),
305                        elements: nested,
306                    }));
307                }
308            }
309        }
310    }
311
312    (elements, running_pos)
313}
314
315fn snapshot_fallback(
316    inner: &TextDocumentInner,
317    frame_dto: &frontend::frame::dtos::FrameDto,
318    effective_kind: crate::highlight::HighlighterKind,
319) -> Vec<FlowElementSnapshot> {
320    let cell_frame_ids = build_cell_frame_ids(inner);
321
322    let block_ids = &frame_dto.blocks;
323    let mut block_dtos: Vec<_> = block_ids
324        .iter()
325        .filter_map(|&id| {
326            block_commands::get_block(&inner.ctx, &{ id })
327                .ok()
328                .flatten()
329        })
330        .collect();
331    let store = inner.ctx.db_context.get_store();
332    crate::inner::refresh_block_positions(&mut block_dtos, store);
333    block_dtos.sort_by_key(|b| b.document_position);
334
335    let mut elements: Vec<FlowElementSnapshot> = block_dtos
336        .iter()
337        .filter_map(|b| crate::text_block::build_block_snapshot(inner, b.id, effective_kind))
338        .map(FlowElementSnapshot::Block)
339        .collect();
340
341    let all_frames = frame_commands::get_all_frame(&inner.ctx).unwrap_or_default();
342    for f in &all_frames {
343        if f.id == frame_dto.id {
344            continue;
345        }
346        if cell_frame_ids.contains(&(f.id as EntityId)) {
347            continue;
348        }
349        if f.parent_frame == Some(frame_dto.id) {
350            if let Some(table_id) = f.table {
351                if let Some(snap) = build_table_snapshot(inner, table_id, effective_kind) {
352                    elements.push(FlowElementSnapshot::Table(snap));
353                }
354            } else {
355                let nested = build_flow_snapshot(inner, f.id as EntityId, effective_kind);
356                elements.push(FlowElementSnapshot::Frame(FrameSnapshot {
357                    frame_id: f.id as usize,
358                    format: frame_dto_to_format(f),
359                    elements: nested,
360                }));
361            }
362        }
363    }
364
365    elements
366}
367
368/// Build a TableSnapshot for the given table ID. Called while lock is held.
369pub(crate) fn build_table_snapshot(
370    inner: &TextDocumentInner,
371    table_id: u64,
372    effective_kind: crate::highlight::HighlighterKind,
373) -> Option<TableSnapshot> {
374    let table_dto = table_commands::get_table(&inner.ctx, &table_id)
375        .ok()
376        .flatten()?;
377
378    let mut cells = Vec::new();
379    for &cell_id in &table_dto.cells {
380        if let Some(cell_dto) = table_cell_commands::get_table_cell(&inner.ctx, &{ cell_id })
381            .ok()
382            .flatten()
383        {
384            let blocks = if let Some(cell_frame_id) = cell_dto.cell_frame {
385                crate::text_block::build_blocks_snapshot_for_frame(
386                    inner,
387                    cell_frame_id,
388                    effective_kind,
389                )
390            } else {
391                Vec::new()
392            };
393            cells.push(CellSnapshot {
394                row: to_usize(cell_dto.row),
395                column: to_usize(cell_dto.column),
396                row_span: to_usize(cell_dto.row_span),
397                column_span: to_usize(cell_dto.column_span),
398                format: cell_dto_to_format(&cell_dto),
399                blocks,
400            });
401        }
402    }
403
404    Some(TableSnapshot {
405        table_id: table_id as usize,
406        rows: to_usize(table_dto.rows),
407        columns: to_usize(table_dto.columns),
408        column_widths: table_dto.column_widths.iter().map(|&v| v as i32).collect(),
409        format: table_dto_to_format(&table_dto),
410        cells,
411    })
412}
413
414/// Build a TableSnapshot with computed positions for cell blocks, starting from
415/// `start_pos`. Returns `(snapshot, running_pos_after_last_cell_block)`.
416///
417/// Cells are processed in row-major order, and block positions within each cell
418/// are computed sequentially — matching `find_block_at_position_sequential` in
419/// the editing use cases.
420fn build_table_snapshot_with_positions(
421    inner: &TextDocumentInner,
422    table_id: u64,
423    start_pos: usize,
424    effective_kind: crate::highlight::HighlighterKind,
425) -> Option<(TableSnapshot, usize)> {
426    let table_dto = table_commands::get_table(&inner.ctx, &table_id)
427        .ok()
428        .flatten()?;
429
430    // Collect and sort cell DTOs in row-major order
431    let mut cell_dtos: Vec<_> = table_dto
432        .cells
433        .iter()
434        .filter_map(|&cell_id| {
435            table_cell_commands::get_table_cell(&inner.ctx, &{ cell_id })
436                .ok()
437                .flatten()
438        })
439        .collect();
440    cell_dtos.sort_by(|a, b| a.row.cmp(&b.row).then(a.column.cmp(&b.column)));
441
442    let mut running_pos = start_pos;
443    let mut cells = Vec::with_capacity(cell_dtos.len());
444    for cell_dto in &cell_dtos {
445        let blocks = if let Some(cell_frame_id) = cell_dto.cell_frame {
446            let (snaps, new_pos) =
447                crate::text_block::build_blocks_snapshot_for_frame_with_positions(
448                    inner,
449                    cell_frame_id,
450                    running_pos,
451                    effective_kind,
452                );
453            running_pos = new_pos;
454            snaps
455        } else {
456            Vec::new()
457        };
458        cells.push(CellSnapshot {
459            row: to_usize(cell_dto.row),
460            column: to_usize(cell_dto.column),
461            row_span: to_usize(cell_dto.row_span),
462            column_span: to_usize(cell_dto.column_span),
463            format: cell_dto_to_format(cell_dto),
464            blocks,
465        });
466    }
467
468    Some((
469        TableSnapshot {
470            table_id: table_id as usize,
471            rows: to_usize(table_dto.rows),
472            columns: to_usize(table_dto.columns),
473            column_widths: table_dto.column_widths.iter().map(|&v| v as i32).collect(),
474            format: table_dto_to_format(&table_dto),
475            cells,
476        },
477        running_pos,
478    ))
479}
480
481// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
482// DTO → public format conversions
483// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
484
485pub(crate) fn frame_dto_to_format(f: &frontend::frame::dtos::FrameDto) -> FrameFormat {
486    FrameFormat {
487        height: f.fmt_height.map(|v| v as i32),
488        width: f.fmt_width.map(|v| v as i32),
489        top_margin: f.fmt_top_margin.map(|v| v as i32),
490        bottom_margin: f.fmt_bottom_margin.map(|v| v as i32),
491        left_margin: f.fmt_left_margin.map(|v| v as i32),
492        right_margin: f.fmt_right_margin.map(|v| v as i32),
493        padding: f.fmt_padding.map(|v| v as i32),
494        border: f.fmt_border.map(|v| v as i32),
495        position: f.fmt_position.clone(),
496        is_blockquote: f.fmt_is_blockquote,
497    }
498}
499
500pub(crate) fn table_dto_to_format(t: &frontend::table::dtos::TableDto) -> crate::flow::TableFormat {
501    crate::flow::TableFormat {
502        border: t.fmt_border.map(|v| v as i32),
503        cell_spacing: t.fmt_cell_spacing.map(|v| v as i32),
504        cell_padding: t.fmt_cell_padding.map(|v| v as i32),
505        width: t.fmt_width.map(|v| v as i32),
506        alignment: t.fmt_alignment.clone(),
507    }
508}
509
510pub(crate) fn cell_dto_to_format(
511    c: &frontend::table_cell::dtos::TableCellDto,
512) -> crate::flow::CellFormat {
513    use frontend::common::entities::CellVerticalAlignment as BackendCVA;
514    crate::flow::CellFormat {
515        padding: c.fmt_padding.map(|v| v as i32),
516        border: c.fmt_border.map(|v| v as i32),
517        vertical_alignment: c.fmt_vertical_alignment.as_ref().map(|v| match v {
518            BackendCVA::Top => crate::flow::CellVerticalAlignment::Top,
519            BackendCVA::Middle => crate::flow::CellVerticalAlignment::Middle,
520            BackendCVA::Bottom => crate::flow::CellVerticalAlignment::Bottom,
521        }),
522        background_color: c.fmt_background_color.clone(),
523    }
524}