Tabellen-Umbenennung schreibt Referenzen dokumentweit um

Bisher speicherte eine Formel den Tabellennamen nur als Text im
Qualifier ("Umsatz!B2") – beim Umbenennen der Tabelle wurde nirgendwo
danach gesucht, sodass jede andere Formel, die noch den alten Namen
nannte, beim nächsten Neuberechnen #REF zeigte.

renameObject() ruft jetzt vor der eigentlichen Umbenennung eine neue
renameDocumentRefs()/rewriteRefsForRename() auf, die dieselbe
Token-Splice-Technik wiederverwendet, die für das dokumentweite
Umschreiben bei Zeilen-/Spalten-Einfügen bereits existiert: jede Formel
im Dokument wird tokenisiert, jeder Qualifier über die echte
wb.resolveTableName-Auflösung (nicht per String-Vergleich) auf die
umbenannte Tabelle geprüft, und nur die Tabellen-Namens-Textspanne
ersetzt – bei der 3-teiligen "Blatt!Tabelle::Zelle"-Form bleibt der
Blatt-Teil unangetastet, ein neuer Name mit Leerzeichen wird korrekt
gequotet. Umbenennen + Umschreiben zählen als ein Undo-Schritt.

Bewusst auf Tabellenzellen begrenzt, wie schon beim Zeilen-/Spalten-
Umschreiben (keine Makro-Aktionen, keine Funktionskörper).

Live per Playwright verifiziert: "Umsatz" per Doppelklick zu "Verkauf"
umbenannt, =SUM(Umsatz!B2:E2) in einer anderen Tabelle wurde automatisch
zu =SUM(Verkauf!B2:E2), Wert blieb unverändert (kein #REF).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-09 12:10:23 +02:00
co-authored by Claude Sonnet 5
parent 3ae3d81066
commit 47c99bbeb6
3 changed files with 304 additions and 13 deletions
+140 -6
View File
@@ -413,6 +413,113 @@ function rewriteRefsForStructuralChange(
return '=' + out;
}
/** `'` needs quoting whenever a name isn't a bare identifier (spaces,
* punctuation, ...) -- shared by the table- and sheet-qualifier cases.
* Module-level (not a store method) since it's pure and both
* `rewriteRefsForRename` below and the store's own `qualifyRef` need it. */
function quoteQualifier(name: string): string {
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "''")}'`;
}
/**
* Rewrite every reference to `targetTableId` inside one formula so its
* qualifier text uses `newName` instead of whatever name it was written
* with -- called when a table is renamed (see `renameObject`), so a
* formula elsewhere that still says `AlterName!B2` keeps pointing at the
* SAME table instead of becoming `#REF` the next time the workbook
* rebuilds ("Referenzen in Formeln müssen immer konsistent sein").
*
* Same token-splice technique and qualifier-detection shape as
* `rewriteRefsForStructuralChange` above (only the qualifier token's own
* text is touched, everything else -- including whitespace and the cell/
* range part -- stays byte-identical), but simpler: no coordinate
* shifting, and only the TABLE-name token is ever replaced -- a 3-part
* `Blatt!Tabelle::Zelle` form keeps its own sheet qualifier untouched,
* only `Tabelle` becomes `newName`. `homeTableId` is the formula's own
* table, needed the same way as in the structural-change rewriter, to
* resolve a same-name-on-two-sheets ambiguity via the real
* `wb.resolveTableName` rules rather than a naive string match.
*/
function rewriteRefsForRename(
raw: string,
homeTableId: string,
targetTableId: string,
newName: string,
wb: Workbook,
): string {
if (!raw.startsWith('=')) return raw;
const body = raw.slice(1);
let toks: Token[];
try {
toks = tokenize(body);
} catch {
return raw; // unparsable -- leave as-is, the evaluator already surfaces #ERR
}
const savedHome = wb.homeTableId;
wb.homeTableId = homeTableId;
const resolve = (name: string, sheetName: string | null): string | null =>
wb.resolveTableName(name, sheetName);
interface Repl {
pos: number;
end: number;
text: string;
}
const repls: Repl[] = [];
let i = 0;
while (i < toks.length && toks[i].type !== 'EOF') {
const tok = toks[i];
let cellIdx = -1;
let resolvedTableId: string | null = null;
let qualTok: Token | null = null;
if ((tok.type === 'IDENT' || tok.type === 'QUOTED') && toks[i + 1]) {
if (toks[i + 1].type === 'BANG' && looksLikeSheetThenTable(toks, i + 1)) {
qualTok = toks[i + 2]; // the TABLE token of the 3-part form -- toks[i]/tok is the sheet, left alone
resolvedTableId = resolve(qualTok.value, tok.value);
cellIdx = i + 4; // consumed: IDENT/QUOTED "!" IDENT/QUOTED "::"
} else if (toks[i + 1].type === 'BANG' || toks[i + 1].type === 'DBLCOLON') {
qualTok = tok;
resolvedTableId = resolve(tok.value, null);
cellIdx = i + 2;
}
}
if (cellIdx === -1) {
i++; // a bare, unqualified cell never needs rewriting on rename -- it's already "this table"
continue;
}
const cellTokA = toks[cellIdx];
if (!cellTokA || cellTokA.type !== 'IDENT' || !looksLikeCell(cellTokA.value)) {
i++; // qualifier not actually followed by a cell -- malformed, leave alone
continue;
}
const isRangeStart = toks[cellIdx + 1]?.type === 'COLON';
const cellTokB = isRangeStart ? toks[cellIdx + 2] : undefined;
const validRange = !!cellTokB && cellTokB.type === 'IDENT' && looksLikeCell(cellTokB.value);
if (resolvedTableId === targetTableId && qualTok) {
repls.push({ pos: qualTok.pos, end: qualTok.pos + qualTok.value.length, text: quoteQualifier(newName) });
}
i = validRange ? cellIdx + 3 : cellIdx + 1;
}
wb.homeTableId = savedHome;
if (repls.length === 0) return raw;
repls.sort((x, y) => x.pos - y.pos);
let out = '';
let cursor = 0;
for (const r of repls) {
out += body.slice(cursor, r.pos) + r.text;
cursor = r.end;
}
out += body.slice(cursor);
return '=' + out;
}
function loadInitial(): DocumentState {
try {
const raw = localStorage.getItem(STORAGE_KEY);
@@ -927,7 +1034,15 @@ function createStore() {
.filter((t) => t.id !== objId)
.map((t) => t.name.toLowerCase()),
);
o.name = others.has(clean.toLowerCase()) ? uniqueName(clean, others) : clean;
const newName = others.has(clean.toLowerCase()) ? uniqueName(clean, others) : clean;
// Rewrite every OTHER formula's qualifier BEFORE renaming -- wb still
// resolves the old name at this point, and rewriteRefsForRename needs
// that to identify which references actually point at this table.
// "Referenzen in Formeln müssen immer konsistent sein" (user request):
// without this, a formula elsewhere still saying the old name would
// silently become #REF the moment the workbook rebuilds below.
if (newName !== o.name) renameDocumentRefs(objId, newName);
o.name = newName;
syncWorkbook(); // formulas referencing the old/new name must re-resolve
} else {
o.name = clean;
@@ -1860,6 +1975,30 @@ function createStore() {
}
}
/** Same driver shape as `rewriteDocumentRefs` above, for a table rename
* (see `rewriteRefsForRename`'s own doc comment) -- every OTHER table's
* cells are walked too, not just the renamed table's own, since a
* qualified reference to it can live anywhere in the document. Scoped
* to table cells only, same as the structural-change rewriter (macro
* bodies address a table by a plain string, not a formula qualifier, and
* are resolved fresh at each run anyway -- see `resolveMacroTableRef` --
* so they need no rewriting; a custom function's own body is likewise
* left alone, matching the existing, established scope of the sibling
* row/col-insert/delete rewrite this mirrors). Called BEFORE the
* object's `.name` is actually changed, so `wb` (not yet rebuilt) still
* resolves the OLD name to `targetTableId`. */
function renameDocumentRefs(targetTableId: string, newName: string) {
for (const s of doc.sheets) {
for (const o of s.objects) {
if (o.kind !== 'table') continue;
for (const [key, raw] of Object.entries(o.cells)) {
const next = rewriteRefsForRename(raw, o.id, targetTableId, newName, wb);
if (next !== raw) o.cells[key] = next;
}
}
}
}
/** Re-key every entry of a "col,row" map, shifting the given axis at/after
* `at` by `delta` (dropping entries whose axis value === `at` when
* `delta` is -1 -- that row/col's own cells are gone). Shared by
@@ -2360,11 +2499,6 @@ function createStore() {
.map((f) => ({ name: f.name, hint: `${f.name}(${f.params.join('; ')})`, kind: 'custom' }));
return [...builtins, ...custom].sort((a, b) => a.name.localeCompare(b.name));
}
/** `'` needs quoting whenever a name isn't a bare identifier (spaces,
* punctuation, ...) -- shared by the table- and sheet-qualifier cases. */
function quoteQualifier(name: string): string {
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "''")}'`;
}
function qualifyRef(homeTableId: string, targetTableId: string, body: string): string {
if (targetTableId === homeTableId) return body;
const target = tableById(targetTableId);