Whole-column and whole-row references (B:B, A:C, 1:1, 2:5)
Formulas accept B:B / A:C / $B:$B and 1:1 / 2:5 / $3:$3, also qualified
(Umsatz!B:B, Umsatz::1:1, 'Blatt 2'!Kosten::A:C). The open end is clamped
to the table's current size wherever cells are read (evaluation, the
dependency graph, SETVALUE targets, FOR sources), and typing past the
table's end now rebuilds the workbook so such a reference covers the new
cells. The Name Box selects the whole column/row.
The token-level rewriters handle the new shape: copy/fill shifts B:B only
sideways and 1:1 only up/down ($ stays), column insert/delete moves or
#REFs B:B like a range (rows leave it alone, and vice versa), and a table
rename rewrites its qualifier.
Also fixes renaming a table referenced with a quoted name ('Tabelle 1'!B2):
the rewriter replaced only as many characters as the unquoted name has,
leaving a stray quote (=Daten1'!B2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -174,6 +174,70 @@ export function currentIdentifierAtCaret(
|
||||
return { text: last.value, start: last.pos + 1 };
|
||||
}
|
||||
|
||||
// ---- whole-column / whole-row references (`B:B`, `A:C`, `1:1`, `$2:$5`) ----
|
||||
|
||||
/** Stand-in for "the last row/column" in a whole-column/whole-row range's
|
||||
* open end. Every consumer clamps it to the actual table size (see
|
||||
* {@link rangeCells}); a consumer that merely clamps to its table (the
|
||||
* Name Box selection) gets the right answer for free. */
|
||||
export const WHOLE_LINE_END = 9_999_998;
|
||||
|
||||
export type LineAxis = 'col' | 'row';
|
||||
|
||||
const COL_PART_RE = /^(\$?)([A-Za-z]{1,3})$/;
|
||||
const ROW_PART_RE = /^(\$?)([0-9]{1,7})$/;
|
||||
|
||||
/** One side of a whole-line reference: `"B"`/`"$B"` (a column) or
|
||||
* `"3"`/`"$3"` (a 1-based row) -> its 0-based index, or `null`. */
|
||||
export function parseLinePart(text: string): { axis: LineAxis; index: number; abs: boolean } | null {
|
||||
let m = COL_PART_RE.exec(text);
|
||||
if (m) return { axis: 'col', index: colToNumber(m[2]), abs: m[1] === '$' };
|
||||
m = ROW_PART_RE.exec(text);
|
||||
if (m && Number(m[2]) >= 1) return { axis: 'row', index: Number(m[2]) - 1, abs: m[1] === '$' };
|
||||
return null;
|
||||
}
|
||||
|
||||
export function formatLinePart(axis: LineAxis, index: number, abs: boolean): string {
|
||||
return (abs ? '$' : '') + (axis === 'col' ? numberToCol(index) : String(index + 1));
|
||||
}
|
||||
|
||||
/** If `toks[i]`, `toks[i+1]`, `toks[i+2]` spell a whole-line range (`B:B`,
|
||||
* `A:C`, `1:1`, `$2:$5` -- both sides the same axis), return its parts.
|
||||
* Shared by the parser and every token-level formula rewriter (copy,
|
||||
* row/column insert/delete, table rename), so they all agree on the shape. */
|
||||
export function wholeLineAt(
|
||||
toks: Token[],
|
||||
i: number,
|
||||
): { axis: LineAxis; a: Token; b: Token; start: number; end: number; startAbs: boolean; endAbs: boolean } | null {
|
||||
const a = toks[i];
|
||||
const colon = toks[i + 1];
|
||||
const b = toks[i + 2];
|
||||
if (!a || !colon || !b || colon.type !== 'COLON') return null;
|
||||
if ((a.type !== 'IDENT' && a.type !== 'NUMBER') || (b.type !== 'IDENT' && b.type !== 'NUMBER')) return null;
|
||||
const pa = parseLinePart(a.value);
|
||||
const pb = parseLinePart(b.value);
|
||||
if (!pa || !pb || pa.axis !== pb.axis) return null;
|
||||
return { axis: pa.axis, a, b, start: pa.index, end: pb.index, startAbs: pa.abs, endAbs: pb.abs };
|
||||
}
|
||||
|
||||
/** The cells a range covers, with a whole-column/whole-row range's open
|
||||
* end clamped to its table's current size (`size` null = unknown table ->
|
||||
* no cells). An ordinary range is just {@link expandRange}. */
|
||||
export function rangeCells(
|
||||
r: { start: { col: number; row: number }; end: { col: number; row: number }; whole?: LineAxis },
|
||||
size: { cols: number; rows: number } | null,
|
||||
): Array<{ col: number; row: number }> {
|
||||
if (!r.whole) return expandRange(r.start, r.end);
|
||||
if (!size) return [];
|
||||
const clamp = (v: number, n: number) => Math.min(v, n - 1);
|
||||
const start = { col: Math.min(r.start.col, r.end.col), row: Math.min(r.start.row, r.end.row) };
|
||||
const end = { col: Math.max(r.start.col, r.end.col), row: Math.max(r.start.row, r.end.row) };
|
||||
// a whole line entirely beyond the table covers nothing
|
||||
if (r.whole === 'col' && start.col >= size.cols) return [];
|
||||
if (r.whole === 'row' && start.row >= size.rows) return [];
|
||||
return expandRange(start, { col: clamp(end.col, size.cols), row: clamp(end.row, size.rows) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a rectangular range (given by two corners, any order) into the list of
|
||||
* cell coordinates it covers, row-major.
|
||||
|
||||
Reference in New Issue
Block a user