Support $-locked references: relative, mixed and absolute
The engine already anticipated $A1/A$1/$A$1 in its grammar comment, but the
lexer had no rule for '$' at all -- any formula using it failed to parse.
- lexer.ts: '$' is now a valid word character, so "$A$1" tokenizes as one
IDENT, same as any other bare word (the lexer still doesn't interpret it,
per its "dumb lexer" design -- the parser/evaluator stay untouched, since
evaluation is purely positional and $ has no effect on a value).
- refs.ts: parseA1()/toA1() now carry colAbs/rowAbs through the $ markers
(backward compatible -- existing {col,row} destructuring is unaffected).
Added cycleReferenceLock() for Excel/Numbers-style F4: relative ->
absolute -> row-locked -> col-locked -> relative, toggling whichever
reference sits at the caret (and the correct side of an A1:B2 range).
- document.ts: shiftFormula() now honors the lock on copy (a locked axis
stays exactly as typed) but not on cut (a moved cell's reference follows
it regardless of $, since cut relocates the cell itself rather than
filling a relative offset).
- TableGrid.vue's inline cell editor and FormulaBar.vue both wire up F4 via
the shared cycleReferenceLock() helper.
- Tests: lexer/parser coverage for $ tokenizing/parsing, a new refs.spec.ts
for parseA1/toA1 round-tripping and the F4 cycle (incl. range side-
picking), and document.spec.ts cases for locked-axis copy vs. cut.
- README.md / src/engine/README.md: documented the $ semantics, F4, and
corrected a couple of now-stale claims (test count, "$ is ignored, there's
no copy/fill", "active cell is always the last-clicked one").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+13
-11
@@ -99,14 +99,16 @@ interface ClipboardData {
|
||||
* Rewrite every cell reference in a formula by (dCol, dRow) -- the same
|
||||
* relative-reference adjustment Excel/Numbers apply on copy.
|
||||
*
|
||||
* `restrict`, when given (a cut/move), narrows this to *only* references that
|
||||
* point inside that rectangle -- i.e. cells that moved along with the formula.
|
||||
* A cut otherwise leaves references untouched, since the cells they point at
|
||||
* did not move (and a qualified `Table!A1` reference is never "inside" a
|
||||
* local rectangle, so it is always left alone during a cut).
|
||||
* A `$`-locked axis (`$A1`, `A$1`, `$A$1`) is exempt from a *copy*'s shift --
|
||||
* that is the whole point of locking a reference -- but not from a *cut*'s:
|
||||
* a cut moves the referenced cell itself, so any reference to it, `$` or not,
|
||||
* must follow to keep pointing at the same (now relocated) cell.
|
||||
*
|
||||
* The formula engine has no absolute (`$A$1`) references, so every local
|
||||
* reference is relative and eligible for this adjustment.
|
||||
* `restrict`, when given (a cut/move), narrows the shift to *only* references
|
||||
* that point inside that rectangle -- i.e. cells that moved along with the
|
||||
* formula. A cut otherwise leaves references untouched, since the cells they
|
||||
* point at did not move (and a qualified `Table!A1` reference is never
|
||||
* "inside" a local rectangle, so it is always left alone during a cut).
|
||||
*/
|
||||
function shiftFormula(raw: string, dCol: number, dRow: number, restrict?: Rect): string {
|
||||
if (!raw.startsWith('=') || (dCol === 0 && dRow === 0)) return raw;
|
||||
@@ -127,7 +129,7 @@ function shiftFormula(raw: string, dCol: number, dRow: number, restrict?: Rect):
|
||||
const prev = toks[i - 1];
|
||||
const qualified = !!prev && (prev.type === 'BANG' || prev.type === 'DBLCOLON');
|
||||
if (restrict && qualified) continue; // cut: a reference into another table never moved
|
||||
const { col, row } = parseA1(tok.value);
|
||||
const { col, row, colAbs, rowAbs } = parseA1(tok.value);
|
||||
if (
|
||||
restrict &&
|
||||
!qualified &&
|
||||
@@ -135,9 +137,9 @@ function shiftFormula(raw: string, dCol: number, dRow: number, restrict?: Rect):
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const nc = Math.max(0, col + dCol);
|
||||
const nr = Math.max(0, row + dRow);
|
||||
out += body.slice(last, tok.pos) + toA1(nc, nr);
|
||||
const nc = !restrict && colAbs ? col : Math.max(0, col + dCol);
|
||||
const nr = !restrict && rowAbs ? row : Math.max(0, row + dRow);
|
||||
out += body.slice(last, tok.pos) + toA1(nc, nr, colAbs, rowAbs);
|
||||
last = tok.pos + tok.value.length;
|
||||
}
|
||||
out += body.slice(last);
|
||||
|
||||
Reference in New Issue
Block a user