Skip to content

Sheet transfer: preserve workbook references (builds on #1803 and #1200) - #1813

Draft
derechopiano wants to merge 19 commits into
storytold:mainfrom
derechopiano:feat/sheet-transfer-links-1739
Draft

derechopiano wants to merge 19 commits into
storytold:mainfrom
derechopiano:feat/sheet-transfer-links-1739

Conversation

@derechopiano

@derechopiano derechopiano commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Moving or copying a sheet that refers to sheets left behind currently has to be refused by #1803. This follow-up preserves supported cell and defined-name references through Workbook Links instead.

Dependencies and review scope

Stacked draft: builds on #1803 and #1200; not ready to merge independently. #1200 includes #464 by @neilcawse and #829 by @marcoarnulfo, with their original commits/authorship retained. This branch merges those foundations rather than implementing a second link model or refresh runtime.

The full main-branch diff includes these prerequisites. Our follow-up is merge commit 3e7d0dc, primarily crates/engine/src/sheet_transfer.rs, its integration tests and transfer documentation. To isolate the transfer integration from the prerequisite code, use git show --remerge-diff 3e7d0dc.

Result

  • Copied/moved cell formulas and imported names can refer back to sheets left behind. For moves, remaining formulas and names can link to the moved sheet in its destination, including a renamed sheet.
  • Existing external links receive destination link numbers; relative paths retain their original base, and preserved package-part indices are cleared when importing caches.
  • New links cache referenced values from the open workbook snapshot, using the existing cache limit. Transfer performs no file reads or automatic saves. Later refresh uses Workbook Links: update linked workbooks from their files, change source, break links, startup prompt (builds on #464 and #829) #1200's existing controls.
  • Copies leave the source unchanged. Moves retain linked Undo/Redo across both workbooks; unsupported cases fail before either document is changed.

Limits

A required link target needs a distinct absolute XLSX path. Unsupported table, multi-sheet, INDIRECT and worksheet-object dependencies remain explicit refusals, including unsupported external references hidden behind names. This is not live recalculation between open workbooks. Save both workbooks before refreshing moved-sheet links from disk.

Validation

  • 25 sheet-transfer tests and 15 existing linked-workbook/Workbook Links integration tests passed.
  • Full cargo xtask ci on Windows: all eight checks passed (fmt, clippy, workspace tests, assets, layers, wasm, parity, keytips).
  • The pre-commit and pre-push hooks reran the complete CI suite successfully.
  • Workbook-link path assertions cover host-native separators and relative Windows-style paths; path resolution behaviour is unchanged. GitHub Ubuntu CI also passed the complete quality gate at b91e250 (run 39089929280).
  • A local merge check against main 19f4387 succeeded without conflicts. Local CI was run on this PR's head, not on a newly committed merge with that main revision.

Related: #1739. #1803 remains the initial self-contained transfer PR.

neilcawse and others added 19 commits October 13, 2026 13:12
…with the link

A formula reading another workbook ([1]Prices!B2, '[2]P&L'!A1:B9, a name over
such a range) recalculated to #REF!: the externalLink parts were passed through
on save but never read. Excel calculates these from the cached copy of the
linked cells it saves in the file, as it does whenever the source is closed.
On a 4,224-file real-world corpus this was the largest root cause of
calculation mismatches.

The reader now loads each link's sheet names, cached cells, refresh-error
flags and defined names into Workbook::external_books; the lexer reads
unquoted [1]Sheet!A1 and [1]!Name; references, linked names and [0]!Name
evaluate from that data; the *IF(S) range arguments refuse
linked-workbook values and written errors, as Excel does with a closed
source; and linked sheets print unquoted when the sheet
name allows, so saved formulas keep their text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… prints back

References into a linked workbook became value arrays when evaluated, so `@`
and implicit intersection took the top-left value (`=@[1]Data!B:B` in row 4
gave the header), ROWS/COLUMNS/INDEX saw the block trimmed to the last cached
cell (`ROWS([1]Data!A:A)` = 3), and every lookup rebuilt the whole block.
They are now Areas whose sheet number (from LINKED_SHEETS up) stands for the
linked workbook and sheet; cells are read from the saved values only when the
reference is dereferenced (Evaluator::cell, Evaluator::block_values), and a
linked sheet's extent is remembered per pass like a local used range.

The printer dropped the quotes from '[1]Jan:Dec'!A1 and the lexer couldn't
read [1]Jan:Dec!A1. The lexer now accepts the unquoted MS-XLSX sheet-range
form, and the printer quotes a range when either sheet name needs it.
…]Jan:Dec

OFFSET took a reference into a linked workbook as its base and read the
saved link values. Excel errors there while the source is closed: the Enron
West NatGas Prices cells wrap OFFSET('[21]Curve Summary'!...) in ISERROR and
cached the backup value. OFFSET now answers #VALUE! for a linked base, the
answer of the other reference-only parameters given a closed workbook's values.

Renaming a local sheet rewrote the second end of [1]Jan:Dec!A1, which names a
sheet of the linked workbook; rename_sheet and delete_sheet now skip spans
whose first end is in a linked workbook.

Workbook::external_books is behind an Arc, like sheets and preserved parts,
so each edit's workbook clone and the undo history share the link cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ixes; [0] names stored local; CF/DV refuse other workbooks; paste into another workbook drops [n]
's [n] check

storytold#829 was written against an older storytold#464. Its typed references by file name or path
(`[Source.xlsx]Sheet1!A1`, `'C:\links\[Source.xlsx]Sheet1'!A1`) were refused by the
entry check storytold#464 added since (a bracketed prefix that isn't a link index). The check now
lets a typed file through where entry links it (`typed_book`), and linking runs last in
`input_to_cell`, so a formula refused for another reason never leaves a new link behind.
Conditional formats, data validation and paste still refuse other workbooks.

A typed file must look like one (a folder or an extension), so `[-1]Sheet1`, `[ 1]Sheet1`
and `[Book2]Sheet1` stay refused as storytold#464's review asked. `[1]!Rate` prints without quotes
around `[1]`, and the shown name of a link (`'C:\links\Source.xlsx'!Rate`) is quoted once
by the printer, as main now quotes sheet-qualified names itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inks, startup prompt

The engine side of Data › Queries & Connections › Workbook Links and File › Info › Edit Links
to Files. Every action is a command, reachable from the CLI and MCP:

- data.workbookLinks / data.editLinks list the links (file, path, type, update, status, last
  refreshed, formulas using it) and ask the UI for the pane or the dialog.
- data.refreshLinks {link?} (Update Values / Refresh) reads the source workbook with
  gridcraft-xlsx: the cells and names this workbook reads from it, including whole columns
  trimmed to the source's used range, names used inside linked names, and 3-D spans. A source
  saved without values is calculated first. One undo step; dependents recalculate.
- data.checkLinkStatus: OK, Error: Source not found, Error: Worksheet not found, Warning:
  Values not updated (the file has other values than the link), Error: Undetermined.
- data.changeLinkSource {link, path} points the link at another file and reads it; onto a file
  another link reads, the two links become one.
- data.breakLink {link?}: as observed in Excel, every cell formula that reads the link, also
  through a defined name, becomes its value (arrays become the values of their range); names
  stay and keep the link. A link nothing names any more is removed and the later [n] move up.
- data.linkStartupPrompt {mode}: userSet / never / always, saved as workbookPr updateLinks
  (ECMA-376 ST_UpdateLinks); the default isn't written.
- data.openLinkSource opens the source; data.enableLinkUpdates is the security warning's
  Enable Content.

Opening a workbook never reads its links: like Excel, their automatic update is disabled until
Enable Content or Update Values (not at all when the prompt says never). Status and last refresh
are session state (DocState::links). Files are read through Session::link_files, the file system
off wasm; in the browser the commands that read sources are disabled with a reason.

Saving: ExternalBook::kept (replacing storytold#829's `added`) says which link part of the file holds a
link unchanged. Unchanged links are still written back byte for byte; a link added, refreshed or
moved to another source is written from the model in its place, and the part of a removed link
is dropped. Save As rebases relative paths of links written from the model too (storytold#824).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…warning

Data gets Excel 365's Queries & Connections group: the Refresh All split button (Refresh All,
Refresh, and Refresh Status, Cancel Refresh, Connection Properties… shown disabled), then
Queries & Connections, Properties (disabled) and Workbook Links, which is off while the workbook
links to no other workbook, as in Excel. Key tip A K now runs Workbook Links (it was Edit Links).

Workbook Links opens the task pane: Refresh All, Break All, a search box and a card per linked
workbook with its location, status (OK, Warning: Values not updated, Error: Source not found…),
last refresh and Refresh, Open Workbook, Change Source… and Break Links. File › Info shows
Related Documents › Edit Links to Files for a workbook with links, which opens the classic Edit
Links dialog (Source, Type, Update, Status; Update Values, Change Source…, Open Source, Break
Link, Check Status, Startup Prompt…). Break Link asks first; Change Source and Startup Prompt
open over Edit Links and return to it. All of it runs the data.* link commands.

Opening a workbook with links shows the message bar "Security Warning: Automatic update of links
has been disabled" with Enable Content. message_bar.rs is a small reusable mechanism: bars come
from the active document's engine state, each with a command button and a close button.

German labels follow German Excel (Arbeitsmappenlinks, Verknüpfungen mit Dateien bearbeiten,
Werte aktualisieren, Quelle ändern…, Quelle öffnen, Verknüpfung löschen, Status prüfen,
Eingabeaufforderung beim Start…, Sicherheitswarnung, Inhalt aktivieren). Generic words carry a
context (`Workbook Links|Source`) so they don't collide with other features' translations.

The open warning "links to external workbooks were kept; they are not followed or refreshed" is
gone: links are now refreshed on request, and the security warning says what Excel says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
file-format-parity (externalLink parts written from the model when a link changes, workbookPr
updateLinks), gaps §12 (what works and what's left for links), ROADMAP progress log, ui-parity
and target-app-parity rows. An MCP test runs the link commands through execute_command. The
command-sequence fuzzer leaves out the link commands that read files or open a document, as it
does for file.open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

4 participants