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:
@@ -206,9 +206,11 @@ eingebauter Funktionsname: `=MARGE(Umsatz!B2; Umsatz!C2)`.
|
|||||||
Name/Parameter, ein nicht parsender Formelkörper, und ein **Funktions-
|
Name/Parameter, ein nicht parsender Formelkörper, und ein **Funktions-
|
||||||
Zyklus** (A ruft B, B ruft A, auch über mehrere Stufen) — wird abgelehnt,
|
Zyklus** (A ruft B, B ruft A, auch über mehrere Stufen) — wird abgelehnt,
|
||||||
bevor je eine `#ERR`-Zelle entsteht.
|
bevor je eine `#ERR`-Zelle entsteht.
|
||||||
- **Umbenennen/Löschen** aktualisiert **keine** bestehenden Formeltexte —
|
- **Umbenennen/Löschen** aktualisiert **keine** bestehenden Formeltexte.
|
||||||
genau wie beim Umbenennen/Löschen einer Tabelle. Zellen, die die
|
Zellen, die die betroffene Funktion aufrufen, zeigen danach `#NAME?`.
|
||||||
betroffene Funktion aufrufen, zeigen danach `#NAME?`.
|
Anders als bei einer **Tabelle** (s. u. „Referenzen bleiben nach dem
|
||||||
|
Umbenennen konsistent") — eine Funktion hat keine Referenz-Syntax, die
|
||||||
|
sich automatisch umschreiben ließe, nur ihren Aufruf-Namen.
|
||||||
- Ein Aufruf wird **beim Parsen der aufrufenden Zelle in ihre AST eingesetzt**
|
- Ein Aufruf wird **beim Parsen der aufrufenden Zelle in ihre AST eingesetzt**
|
||||||
(Inlining, keine Laufzeit-Closures) — siehe
|
(Inlining, keine Laufzeit-Closures) — siehe
|
||||||
[`src/engine/userFunctions.ts`](src/engine/userFunctions.ts) und die
|
[`src/engine/userFunctions.ts`](src/engine/userFunctions.ts) und die
|
||||||
@@ -765,6 +767,27 @@ Die Formel-**Point-Mode**-Einfügung (s. o.) erkennt eine mehrdeutige
|
|||||||
Zieltabelle selbst und fügt automatisch die volle 3-teilige Form ein, statt
|
Zieltabelle selbst und fügt automatisch die volle 3-teilige Form ein, statt
|
||||||
einer Formel, die versehentlich die falsche Tabelle träfe.
|
einer Formel, die versehentlich die falsche Tabelle träfe.
|
||||||
|
|
||||||
|
**Referenzen bleiben nach dem Umbenennen konsistent.** Wird eine Tabelle
|
||||||
|
umbenannt (Doppelklick auf ihren Namen in der Kopfleiste), schreibt der
|
||||||
|
Store **dokumentweit jede** `Tabelle!Zelle`-/`Tabelle::Zelle`-Referenz auf
|
||||||
|
sie um — egal in welcher anderen Tabelle sie steht, egal ob 2- oder
|
||||||
|
3-teilige Form (bei der 3-teiligen Form wird nur der Tabellen-Teil
|
||||||
|
ersetzt, der Blatt-Teil bleibt unangetastet), egal ob Einzelzelle oder
|
||||||
|
Bereich. Nur die betroffene Qualifier-Textspanne wird ersetzt (nötigenfalls
|
||||||
|
mit `'...'` gequotet, wenn der neue Name Leerzeichen/Sonderzeichen
|
||||||
|
enthält) — der Rest der Formel, inklusive Whitespace, bleibt byte-identisch,
|
||||||
|
dieselbe Technik wie beim dokumentweiten Umschreiben durch Zeilen-/
|
||||||
|
Spalten-Einfügen (s. u.). Umbenennen + Umschreiben zählen zusammen als
|
||||||
|
**ein** Undo-Schritt. Damit bleibt eine Formel nach dem Umbenennen exakt
|
||||||
|
so gültig wie vorher — ohne dieses Umschreiben würde jede Formel, die noch
|
||||||
|
den alten Namen nennt, beim nächsten Neuberechnen `#REF` zeigen (der
|
||||||
|
Tabellenname existiert für sie dann schlicht nicht mehr). Bewusst
|
||||||
|
**begrenzt auf Tabellenzellen** — Makro-Aktionen adressieren eine Tabelle
|
||||||
|
über einen zur Laufzeit aufgelösten Text-String (s. „Makros" oben), nicht
|
||||||
|
über eine Formel-Qualifier-Syntax, und ein benutzerdefinierter
|
||||||
|
Funktionskörper wird ebenfalls nicht durchsucht (dieselbe, bereits
|
||||||
|
bestehende Begrenzung wie beim dokumentweiten Zeilen-/Spalten-Umschreiben).
|
||||||
|
|
||||||
**`$`-Bezüge sperren** (relativ/gemischt/absolut): `A1` (relativ) vs. `$A1` /
|
**`$`-Bezüge sperren** (relativ/gemischt/absolut): `A1` (relativ) vs. `$A1` /
|
||||||
`A$1` (Spalte bzw. Zeile gesperrt) vs. `$A$1` (beide gesperrt). Für die
|
`A$1` (Spalte bzw. Zeile gesperrt) vs. `$A$1` (beide gesperrt). Für die
|
||||||
Auswertung selbst ist `$` irrelevant — sie ist rein positionsbasiert. Es zählt
|
Auswertung selbst ist `$` irrelevant — sie ist rein positionsbasiert. Es zählt
|
||||||
@@ -1133,6 +1156,16 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
|||||||
Bereich, `$`-Sperren bleiben wirkungslos (anders als beim Kopieren),
|
Bereich, `$`-Sperren bleiben wirkungslos (anders als beim Kopieren),
|
||||||
unveränderte Referenzen bleiben byte-identisch, Undo/Redo, Schutz gegen
|
unveränderte Referenzen bleiben byte-identisch, Undo/Redo, Schutz gegen
|
||||||
Löschen der letzten verbleibenden Zeile/Spalte.
|
Löschen der letzten verbleibenden Zeile/Spalte.
|
||||||
|
- **Tabellen-Umbenennung schreibt Referenzen um** — ein `Tabelle!Zelle`-
|
||||||
|
und ein `Tabelle::Zelle`-Bezug aus einer *anderen* Tabelle werden beide
|
||||||
|
umgeschrieben und liefern danach weiterhin denselben Wert (kein `#REF`),
|
||||||
|
ein Bereichsbezug behält seinen Bereich (nur der Qualifier ändert sich),
|
||||||
|
bei der 3-teiligen `Blatt!Tabelle::Zelle`-Form wird ausschließlich der
|
||||||
|
Tabellen-Teil ersetzt, ein neuer Name mit Leerzeichen wird korrekt
|
||||||
|
gequotet, eine Formel, die eine *andere*, nicht umbenannte Tabelle
|
||||||
|
referenziert, bleibt byte-identisch, ein redundanter Selbst-Qualifier
|
||||||
|
innerhalb der umbenannten Tabelle selbst wird ebenfalls erfasst,
|
||||||
|
Umbenennen + Umschreiben zählen als ein Undo-Schritt.
|
||||||
- **Abhängigkeiten erkunden** (`graphPrecedents`/`graphDependents` in
|
- **Abhängigkeiten erkunden** (`graphPrecedents`/`graphDependents` in
|
||||||
`workbook.spec.ts`, `cellGraphSelf`/`cellGraphNeighbors`/
|
`workbook.spec.ts`, `cellGraphSelf`/`cellGraphNeighbors`/
|
||||||
`openDependencyGraph`/`closeDependencyGraph` in `document.spec.ts`) —
|
`openDependencyGraph`/`closeDependencyGraph` in `document.spec.ts`) —
|
||||||
@@ -1314,10 +1347,11 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
|||||||
- **Benutzerdefinierte Funktionen** (Phase 1, siehe oben) bewusst ohne:
|
- **Benutzerdefinierte Funktionen** (Phase 1, siehe oben) bewusst ohne:
|
||||||
Live-Vorschau des Ergebnisses im Bearbeiten-Dialog, variadische Parameter/
|
Live-Vorschau des Ergebnisses im Bearbeiten-Dialog, variadische Parameter/
|
||||||
Default-Werte, und automatisches Nachführen bestehender Aufrufe bei
|
Default-Werte, und automatisches Nachführen bestehender Aufrufe bei
|
||||||
Umbenennung (Verhalten entspricht bewusst dem einer Tabellen-Umbenennung,
|
Umbenennung — anders als bei einer Tabelle inzwischen (s. o.) bewusst
|
||||||
s. o.) — der Funktionsname selbst wird beim Formeltippen aber inzwischen
|
nicht nachgezogen, da eine Funktion nur einen Aufruf-Namen hat, keine
|
||||||
vervollständigt (s. o. „Funktions-Autovervollständigung"), auch für eigene
|
umschreibbare Referenz-Syntax. Der Funktionsname selbst wird beim
|
||||||
Funktionen.
|
Formeltippen aber inzwischen vervollständigt (s. o. „Funktions-
|
||||||
|
Autovervollständigung"), auch für eigene Funktionen.
|
||||||
- **Funktions-Autovervollständigung** bewusst ohne: Zell-/Tabellennamen-
|
- **Funktions-Autovervollständigung** bewusst ohne: Zell-/Tabellennamen-
|
||||||
Vorschläge (dafür existiert der Point-Mode, s. o.), Vervollständigung, die
|
Vorschläge (dafür existiert der Point-Mode, s. o.), Vervollständigung, die
|
||||||
auch Text *nach* dem Cursor berücksichtigt (nur der Text bis zum Cursor
|
auch Text *nach* dem Cursor berücksichtigt (nur der Text bis zum Cursor
|
||||||
|
|||||||
@@ -1244,6 +1244,129 @@ describe('insertRow/deleteRow/insertColumn/deleteColumn (mid-grid, not just edge
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('renaming a table rewrites every other formula\'s qualifier (document-wide, not just names re-resolving)', () => {
|
||||||
|
const rawInput = (s: DocumentStore, table: string, a1: string) => {
|
||||||
|
const { col, row } = coords(a1);
|
||||||
|
return s.cellInput(tableId(s, table), col, row);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('a plain "Alt!B2" reference in ANOTHER table becomes "Neu!B2" and keeps evaluating correctly', () => {
|
||||||
|
const s = fresh();
|
||||||
|
const src = s.addTable(s.doc.activeSheetId);
|
||||||
|
const srcName = s.tableById(src)!.name;
|
||||||
|
const dst = s.addTable(s.doc.activeSheetId);
|
||||||
|
const dstName = s.tableById(dst)!.name;
|
||||||
|
setCell(s, srcName, 'B2', '10');
|
||||||
|
setCell(s, dstName, 'A1', `=${srcName}!B2`);
|
||||||
|
expect(val(s, dstName, 'A1')).toBe(10);
|
||||||
|
|
||||||
|
s.renameObject(src, 'Neu');
|
||||||
|
expect(rawInput(s, dstName, 'A1')).toBe('=Neu!B2');
|
||||||
|
expect(val(s, dstName, 'A1')).toBe(10); // still resolves -- no #REF
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the "::" qualifier form is rewritten too', () => {
|
||||||
|
const s = fresh();
|
||||||
|
const src = s.addTable(s.doc.activeSheetId);
|
||||||
|
const srcName = s.tableById(src)!.name;
|
||||||
|
const dst = s.addTable(s.doc.activeSheetId);
|
||||||
|
const dstName = s.tableById(dst)!.name;
|
||||||
|
setCell(s, dstName, 'A1', `=${srcName}::B2+1`);
|
||||||
|
|
||||||
|
s.renameObject(src, 'Neu');
|
||||||
|
expect(rawInput(s, dstName, 'A1')).toBe('=Neu::B2+1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a range reference keeps its range, only the qualifier changes', () => {
|
||||||
|
const s = fresh();
|
||||||
|
const src = s.addTable(s.doc.activeSheetId);
|
||||||
|
const srcName = s.tableById(src)!.name;
|
||||||
|
const dst = s.addTable(s.doc.activeSheetId);
|
||||||
|
const dstName = s.tableById(dst)!.name;
|
||||||
|
setCell(s, dstName, 'A1', `=SUM(${srcName}!B2:B6)`);
|
||||||
|
|
||||||
|
s.renameObject(src, 'Neu');
|
||||||
|
expect(rawInput(s, dstName, 'A1')).toBe('=SUM(Neu!B2:B6)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only the TABLE part of a 3-part "Blatt!Tabelle::Zelle" reference is rewritten, the sheet stays', () => {
|
||||||
|
const s = fresh();
|
||||||
|
const sheet2 = s.addSheet('Zweites');
|
||||||
|
const src = s.addTable(sheet2);
|
||||||
|
const srcName = s.tableById(src)!.name;
|
||||||
|
s.setActiveSheet(s.doc.sheets[0].id);
|
||||||
|
const dst = s.addTable(s.doc.sheets[0].id);
|
||||||
|
const dstName = s.tableById(dst)!.name;
|
||||||
|
const sheet2Name = s.sheetById(sheet2)!.name;
|
||||||
|
setCell(s, dstName, 'A1', `=${sheet2Name}!${srcName}::B2`);
|
||||||
|
|
||||||
|
s.renameObject(src, 'Neu');
|
||||||
|
expect(rawInput(s, dstName, 'A1')).toBe(`=${sheet2Name}!Neu::B2`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renaming to a name that needs quoting produces a properly quoted qualifier', () => {
|
||||||
|
const s = fresh();
|
||||||
|
const src = s.addTable(s.doc.activeSheetId);
|
||||||
|
const srcName = s.tableById(src)!.name;
|
||||||
|
const dst = s.addTable(s.doc.activeSheetId);
|
||||||
|
const dstName = s.tableById(dst)!.name;
|
||||||
|
setCell(s, dstName, 'A1', `=${srcName}!B2`);
|
||||||
|
|
||||||
|
s.renameObject(src, 'Neuer Name');
|
||||||
|
expect(rawInput(s, dstName, 'A1')).toBe("='Neuer Name'!B2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a formula referencing a DIFFERENT, un-renamed table stays byte-identical', () => {
|
||||||
|
const s = fresh();
|
||||||
|
const src = s.addTable(s.doc.activeSheetId);
|
||||||
|
const other = s.addTable(s.doc.activeSheetId);
|
||||||
|
const otherName = s.tableById(other)!.name;
|
||||||
|
const dst = s.addTable(s.doc.activeSheetId);
|
||||||
|
const dstName = s.tableById(dst)!.name;
|
||||||
|
const untouched = `=${otherName}!B2 + 1`;
|
||||||
|
setCell(s, dstName, 'A1', untouched);
|
||||||
|
|
||||||
|
s.renameObject(src, 'Neu');
|
||||||
|
expect(rawInput(s, dstName, 'A1')).toBe(untouched);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a redundant self-qualified reference inside the renamed table itself is rewritten too', () => {
|
||||||
|
const s = fresh();
|
||||||
|
const id = s.addTable(s.doc.activeSheetId);
|
||||||
|
const name = s.tableById(id)!.name;
|
||||||
|
setCell(s, name, 'B2', '10');
|
||||||
|
setCell(s, name, 'A1', `=${name}!B2`); // redundant explicit self-qualifier
|
||||||
|
|
||||||
|
s.renameObject(id, 'Neu');
|
||||||
|
expect(rawInput(s, 'Neu', 'A1')).toBe('=Neu!B2');
|
||||||
|
expect(val(s, 'Neu', 'A1')).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the rewrite and the rename coalesce into a single undo step', () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
try {
|
||||||
|
const s = fresh();
|
||||||
|
const src = s.addTable(s.doc.activeSheetId);
|
||||||
|
const srcName = s.tableById(src)!.name;
|
||||||
|
const dst = s.addTable(s.doc.activeSheetId);
|
||||||
|
const dstName = s.tableById(dst)!.name;
|
||||||
|
setCell(s, dstName, 'A1', `=${srcName}!B2`);
|
||||||
|
vi.advanceTimersByTime(600); // let the setup settle first
|
||||||
|
|
||||||
|
s.renameObject(src, 'Neu');
|
||||||
|
vi.advanceTimersByTime(600);
|
||||||
|
expect(rawInput(s, dstName, 'A1')).toBe('=Neu!B2');
|
||||||
|
|
||||||
|
s.undo(); // one Ctrl+Z undoes BOTH the rename and the ref rewrite together
|
||||||
|
expect(s.tableById(src)!.name).toBe(srcName);
|
||||||
|
expect(rawInput(s, dstName, 'A1')).toBe(`=${srcName}!B2`);
|
||||||
|
} finally {
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('dependency-graph view (cellGraphSelf/cellGraphNeighbors)', () => {
|
describe('dependency-graph view (cellGraphSelf/cellGraphNeighbors)', () => {
|
||||||
it('cellGraphSelf describes a formula cell: label, raw text, value, table/sheet name', () => {
|
it('cellGraphSelf describes a formula cell: label, raw text, value, table/sheet name', () => {
|
||||||
const s = fresh();
|
const s = fresh();
|
||||||
|
|||||||
+140
-6
@@ -413,6 +413,113 @@ function rewriteRefsForStructuralChange(
|
|||||||
return '=' + out;
|
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 {
|
function loadInitial(): DocumentState {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
@@ -927,7 +1034,15 @@ function createStore() {
|
|||||||
.filter((t) => t.id !== objId)
|
.filter((t) => t.id !== objId)
|
||||||
.map((t) => t.name.toLowerCase()),
|
.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
|
syncWorkbook(); // formulas referencing the old/new name must re-resolve
|
||||||
} else {
|
} else {
|
||||||
o.name = clean;
|
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
|
/** 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
|
* `at` by `delta` (dropping entries whose axis value === `at` when
|
||||||
* `delta` is -1 -- that row/col's own cells are gone). Shared by
|
* `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' }));
|
.map((f) => ({ name: f.name, hint: `${f.name}(${f.params.join('; ')})`, kind: 'custom' }));
|
||||||
return [...builtins, ...custom].sort((a, b) => a.name.localeCompare(b.name));
|
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 {
|
function qualifyRef(homeTableId: string, targetTableId: string, body: string): string {
|
||||||
if (targetTableId === homeTableId) return body;
|
if (targetTableId === homeTableId) return body;
|
||||||
const target = tableById(targetTableId);
|
const target = tableById(targetTableId);
|
||||||
|
|||||||
Reference in New Issue
Block a user