Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/paged-canvas/src/mutate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,16 @@ fn apply_insert_text(
});
}

// A freshly-created table cell (and any other empty paragraph stream)
// carries ZERO paragraphs. `locate` pins an empty stream to
// `paragraph_idx 0` (via `saturating_sub(1)`), and `insert_one_segment`
// would then index an empty slice — the `index out of bounds: len is 0
// but the index is 0` panic the sheet cell-pour hit. Seed one empty
// paragraph so the first write into a new cell has a stream to land in.
if paragraphs.is_empty() {
paragraphs.push(paged_parse::Paragraph::default());
}

// Phase 3 Gap-D: split text on `\n`. Each segment becomes a
// contiguous insert within a (possibly new) paragraph. Multiple
// `\n`s in the source split into multiple paragraphs.
Expand Down
39 changes: 39 additions & 0 deletions crates/paged-canvas/tests/cell_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,45 @@ fn insert_text_into_cell_then_read_back() {
}
}

#[test]
fn pour_text_into_a_freshly_inserted_empty_table_cell_does_not_panic() {
// Regression (the sheet cell-pour panic): a cell created by `insertTable`
// carries ZERO paragraphs. The first text pour into it located the end of
// an empty stream and indexed `paragraphs[0]` on an empty slice —
// `index out of bounds: the len is 0 but the index is 0` in
// `insert_one_segment`. `apply_insert_text` now seeds one empty paragraph
// when the cell stream is empty, so the first write lands cleanly.
let mut model = load_model();
let out = model
.apply_mutation(&Mutation::InsertTable {
story_id: "u10".into(),
rows: 2,
cols: 2,
header_rows: 0,
footer_rows: 0,
column_widths: vec![],
row_heights: vec![],
})
.expect("insert a 2x2 table into the story");
let table_id = match out.created_id {
Some(paged_canvas::ElementId::Table { table_id, .. }) => table_id,
other => panic!("insertTable must report a Table createdId, got {other:?}"),
};
// The brand-new cell (0,0) has no paragraphs; pouring text MUST NOT panic.
model
.apply_mutation(&Mutation::InsertText {
story_id: "u10".into(),
offset: 0,
text: "Hi".into(),
cell: Some(TextCellAddr {
table_id,
row: 0,
col: 0,
}),
})
.expect("pour text into the fresh empty cell");
}

#[test]
fn insert_into_cell_undo_restores_exact_prior_content() {
let mut model = load_model();
Expand Down
38 changes: 38 additions & 0 deletions crates/paged-canvas/tests/tablecell_wire.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Wire pin: the EXACT op shapes paged.sheet's lowering emits for a
// native-table cell pour must deserialize as the wire `Mutation`. (A bundle
// bug once nested the whole Table id as `table_id`, which the engine rejected
// with "invalid type: map, expected a string" — this pins the correct shapes
// so a regression fails loudly here.)

use paged_canvas::Mutation;

fn try_de(label: &str, json: &str) {
match serde_json::from_str::<Mutation>(json) {
Ok(_) => println!("OK {label}"),
Err(e) => println!("FAIL {label}: {e}"),
}
}

#[test]
fn sheet_cell_pour_ops_deserialize() {
// 1. cell fill — setElementProperty on a tableCell, colorRef string value.
try_de(
"cellFillColor",
r#"{"op":"setElementProperty","args":{"elementId":{"kind":"tableCell","id":{"story_id":"u1","table_id":"t1","row":0,"col":0}},"path":"cellFillColor","value":{"type":"colorRef","value":"Color/Black"}}}"#,
);
// 2. edge stroke — setElementProperty on a tableCell, length value.
try_de(
"cellTopEdgeStrokeWeight",
r#"{"op":"setElementProperty","args":{"elementId":{"kind":"tableCell","id":{"story_id":"u1","table_id":"t1","row":0,"col":0}},"path":"cellTopEdgeStrokeWeight","value":{"type":"length","value":0.5}}}"#,
);
// 3. text pour — insertText with the cell qualifier (TextCellAddr).
try_de(
"insertText.cell",
r#"{"op":"insertText","args":{"storyId":"u1","offset":0,"text":"hi","cell":{"tableId":"t1","row":0,"col":0}}}"#,
);
// 4. span — setCellSpan.
try_de(
"setCellSpan",
r#"{"op":"setCellSpan","args":{"storyId":"u1","tableId":"t1","row":0,"col":0,"rowSpan":2,"columnSpan":1}}"#,
);
}
Loading