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:
+140
-6
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user