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:
@@ -564,6 +564,40 @@ describe('clipboard: copy / cut / paste', () => {
|
||||
s.cancelClipboard();
|
||||
expect(s.hasClipboard.value).toBe(false);
|
||||
});
|
||||
|
||||
it('copy leaves $-locked axes exactly as typed; a relative axis still shifts', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'B2', '=$A$1+A$1+$A1+A1');
|
||||
|
||||
s.setSelection({ tableId: id, anchor: coords('B2'), focus: coords('B2') });
|
||||
s.copySelection();
|
||||
s.setSelection({ tableId: id, anchor: coords('D5'), focus: coords('D5') }); // +2 cols, +3 rows
|
||||
s.pasteClipboard();
|
||||
|
||||
// fully locked: untouched -- row-locked: only the column shifts --
|
||||
// col-locked: only the row shifts -- relative: both shift
|
||||
expect(s.cellInput(id, coords('D5').col, coords('D5').row)).toBe('=$A$1+C$1+$A4+C4');
|
||||
});
|
||||
|
||||
it('cutting a $-locked reference still follows the moved cell', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'A1', '5'); // stays put -- referenced, not cut
|
||||
setCell(s, name, 'B1', '10'); // part of the cut block (B1:B2)
|
||||
setCell(s, name, 'B2', '=$B$1+$A$1'); // one $-locked ref moves with the cut, one doesn't
|
||||
|
||||
s.setSelection({ tableId: id, anchor: coords('B1'), focus: coords('B2') });
|
||||
s.cutSelection();
|
||||
s.setSelection({ tableId: id, anchor: coords('D1'), focus: coords('D1') });
|
||||
s.pasteClipboard();
|
||||
|
||||
expect(s.cellInput(id, coords('D2').col, coords('D2').row)).toBe('=$D$1+$A$1');
|
||||
expect(val(s, name, 'D2')).toBe(15);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('text box objects', () => {
|
||||
|
||||
Reference in New Issue
Block a user