Skip to content

feat(odf): write a repeated sheet cell by cutting the run - #858

Merged
andiwand merged 1 commit into
mainfrom
feat/sheet-split-on-write
Sep 7, 2026
Merged

feat(odf): write a repeated sheet cell by cutting the run#858
andiwand merged 1 commit into
mainfrom
feat/sheet-split-on-write

Conversation

@andiwand

@andiwand andiwand commented Sep 7, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

What it does

Sheet::set_cell refused a repeated .ods cell — one element stands for every position the repeat covers, so a write would have hit all of them. It now cuts the run:

<table:table-cell table:number-columns-repeated="4">x</…>
                        ↓  set_cell(2, 0, "y")
  repeated="2" → x  |  the one written → y  |  repeated="1" → x

split_repeat copies the table:table-row and the table:table-cell around the position, and leaves the original node as the one written — so its element, its paragraph and its run all survive, and the write goes through them unchanged. Only the copies need parsing.

reindex_sheet then rebuilds the sheet's position index off the dom. A cell node keeps the element it already carries (matched on the node), a copied one gets a fresh one. That replaces surgical insertion into the flat cells vector with its per-row first_cell offsets, which is the part that would have been easy to get subtly wrong.

It does not reintroduce the expansion

The cost follows the row the write touched, not the grid the repeat claims. A write into a 1048576 × 1024 repeat — #776's bomb — leaves the registry under 32 elements, with the neighbouring positions still reading their old value. A test pins exactly that.

Two costs worth stating, and they are in odf/AGENTS.md: cutting a repeated row copies every cell in it, so elements grow with the row rather than with the repeat; and the reindex walks the row nodes, which a repeat collapses, so it is bounded by the dom rather than the grid.

Refusals stay clean

The formula and markup checks now run before the split, so a refused write leaves the run uncut — the property #856 established, which the obvious ordering would have broken (splitting, then throwing, rewrites the file for nothing). only_text_run splits in two for this: holds_one_run decides without writing, text_run_of creates the run an empty paragraph needs.

A cell the file states no element for still refuses; that is materialising a missing <table:table-cell>, a separate thing.

Why it needed #857 first

A handle taken before the write keeps naming its position and re-resolves to the new element, so it shows the write:

const SheetCell held = sheet.cell(2, 0);
sheet.set_cell(2, 0, CellValue("y"));
held.value().text();   // "y"

Without positional ids that handle would silently have come to mean the left part of the cut run.

Checks

  • 1588 tests, 1582 passed, 6 pre-existing skips. 8 are new: column split (middle and both ends), row split, handle-follows-the-write, save/reopen, refusal-leaves-it-uncut, no-expansion.
  • Reference HTML output byte for byte unchanged. clang-tidy clean; gcc-15 -Wall -Wextra -Werror over five TUs.
  • LibreOffice oracle, by hand on a real .ods: three repeated cells written, saved, and both the original and the result converted with soffice --convert-to csv. 121 lines each, and the only line that differs is the one holding the three cells written — nothing else moved. Not committed, soffice is not in CI.

Base automatically changed from feat/sheet-repeat-identity to main September 7, 2026 19:58
`Sheet::set_cell` refused a repeated cell: one element stands for every
position the repeat covers, so a write would have hit all of them.

`split_repeat` cuts the run instead. The `table:table-row` and the
`table:table-cell` are copied around the position written, the original node
staying as the one written so its element and children survive, and
`reindex_sheet` rebuilds the sheet's position index off the dom - a cell node
keeping the element it already carries, a copied one getting a fresh one. The
positions around it read as they did.

The cost follows the row the write touched, not the grid the repeat claims:
a write into a `1048576 x 1024` repeat leaves the registry under 32 elements,
which is the expansion #776 removed and a test pins.

The formula and markup refusals now run against the cell before the split, so
a refused write leaves the run uncut.

`only_text_run` splits in two: `holds_one_run` decides the refusal without
writing anything, `text_run_of` creates the run an empty paragraph needs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J325TWocZ4iXBjvKv2ZVZi
@andiwand
andiwand force-pushed the feat/sheet-split-on-write branch from 2c5a194 to 8f951d4 Compare September 7, 2026 20:05
@andiwand
andiwand merged commit 0f241d7 into main Sep 7, 2026
36 checks passed
@andiwand
andiwand deleted the feat/sheet-split-on-write branch September 7, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant