Add a sheet qualifier: "Blatt!Tabelle::Zelle", table names unique per sheet

Table names were document-unique; now they're unique per sheet only --
two different sheets may each hold a table called "Umsatz". A plain,
unqualified reference (Umsatz!B2 / Umsatz::B2, unchanged from before)
resolves to the table on the SAME sheet as the formula referencing it
when the name is otherwise ambiguous, falling back to the single
document-wide match when it isn't ambiguous at all -- exactly as
before for every existing formula. The new explicit 3-part form
(Analyse!Umsatz::B2, quoted where a name has spaces) always targets
the named sheet's table regardless of where the formula itself lives,
for the case that's still genuinely ambiguous.

Engine: parser.ts's afterFirstQualifierToken() disambiguates via
lookahead -- "X!Y::" (a name, then "::") is the new 3-part sheet+table
form; a bare "X!Y" keeps its original single-qualifier meaning, so
every formula written before this exists still parses identically.
CellNode/RangeNode gain an optional sheetQualifier field.
Workbook.resolveTableName(name, sheetName?) implements the resolution
policy; TableModel gains sheetName. precedentsOf() now also sets
homeTableId (previously only evaluateCell did), so graph edges use the
same same-sheet-preference resolution an evaluation would.

Store: table name uniqueness checks (addTable, renameObject) are now
scoped to the target sheet instead of the whole document. Point-mode's
qualifyRef() inserts the full "Blatt!Tabelle::" form only when the
clicked table's name is genuinely ambiguous and lives on a different
sheet than the editor; otherwise the simple form, unchanged.

200 tests passing (19 new), vue-tsc and eslint clean, verified live in
the running app: renamed a Blatt-2 table to the same name as Analyse's
seed "Umsatz", confirmed an unqualified reference resolves per-sheet,
an explicit sheet-qualified one always targets the named sheet, and
the original sheet's own formulas are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-02 16:16:21 +02:00
co-authored by Claude Sonnet 5
parent fdde4ed5a7
commit 7340307a70
11 changed files with 483 additions and 67 deletions
+30 -5
View File
@@ -35,8 +35,12 @@ import { evaluate } from './evaluator';
export interface TableModel {
id: string;
/** document-unique table name, used as the qualifier in formulas */
/** table name, used as the qualifier in formulas -- unique *per sheet*
* only; the same name may occur on more than one sheet (see
* `sheetName` and {@link EngineHost.resolveTableName}). */
name: string;
/** name of the sheet this table lives on. */
sheetName: string;
cols: number;
rows: number;
/** key `${col},${row}` -> raw user input. Absent = empty cell. */
@@ -78,10 +82,26 @@ export class Workbook implements EngineHost {
// ---- EngineHost ---------------------------------------------------------
resolveTableName(name: string): string | null {
resolveTableName(name: string, sheetName?: string | null): string | null {
const needle = name.trim().toLowerCase();
const hit = this.tables.find((t) => t.name.trim().toLowerCase() === needle);
return hit ? hit.id : null;
const candidates = this.tables.filter((t) => t.name.trim().toLowerCase() === needle);
if (candidates.length === 0) return null;
if (sheetName != null) {
const sheetNeedle = sheetName.trim().toLowerCase();
const hit = candidates.find((t) => t.sheetName.trim().toLowerCase() === sheetNeedle);
return hit ? hit.id : null;
}
if (candidates.length === 1) return candidates[0].id;
// ambiguous (same name on more than one sheet) -- prefer a table on the
// same sheet as the formula doing the referencing, if there is one
const home = this.tableById(this.homeTableId);
if (home) {
const sameSheet = candidates.find((t) => t.sheetName === home.sheetName);
if (sameSheet) return sameSheet.id;
}
return null; // still ambiguous -> #REF, needs an explicit sheet qualifier
}
tableSize(tableId: string): { cols: number; rows: number } | null {
@@ -167,9 +187,14 @@ export class Workbook implements EngineHost {
/** Resolve every reference in `ast` to concrete cell keys (home = tableId). */
private precedentsOf(ast: Node, tableId: string): string[] {
// resolveTableName()'s "prefer the home sheet" ambiguity fallback reads
// this.homeTableId -- set it here too (not just in evaluateCell) so graph
// edges are wired using the *same* resolution an evaluation would use.
this.homeTableId = tableId;
const out: string[] = [];
for (const ref of collectRefs(ast)) {
const target = ref.qualifier == null ? tableId : this.resolveTableName(ref.qualifier);
const target =
ref.qualifier == null ? tableId : this.resolveTableName(ref.qualifier, ref.sheetQualifier ?? null);
if (!target) continue; // unknown table -> no edge; evaluator emits #REF
if (ref.kind === 'cell') {
out.push(keyOf(target, ref.col, ref.row));