Document-wide, named formula functions (e.g. MARGE(revenue; cost) =
(revenue - cost) / revenue) callable from any cell like a built-in
(=MARGE(Umsatz!B2; Umsatz!C2)). Created, edited and deleted only through
a new "𝑓 Funktionen" toolbar button -> modal (list + shared edit form),
never via JSON handedit.
Engine: a call is inlined into the caller's AST at parse time
(src/engine/userFunctions.ts's expandUserFunctions), so evaluator.ts and
graph.ts need no changes -- dependencies on cells referenced only inside
a function body (not in the call) are picked up for free. parser.ts
gains an optional paramNames argument to parseFormula() producing a new
ParamNode leaf, used only when parsing a function body.
Store: addCustomFunction/updateCustomFunction/deleteCustomFunction with
inline (not toast) validation -- name/parameter shape, no collision with
a built-in or another function, the body parses, and a definition-time
cycle check across the whole call graph. Renaming/deleting a function
mirrors table rename/delete: existing calls are not rewritten, they
become #NAME? on the next rebuild. Fully undo/redo-able and part of the
JSON document (customFunctions: [], migrated for v1 and older v2 docs).
178 tests passing (32 new), vue-tsc and eslint clean. Manually verified
end-to-end in the running app via Playwright: define/call/recalculate,
rename -> #NAME?, a closing cycle rejected inline at save, undo restores
the function library.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1. In-cell editor width/height ("nur in der Breite der Zelle angezeigt"):
the editor now lives as an overlay (like the clip/fill markers) instead
of inside its <td>, so it can grow past the cell -- width first, up to
an approximated display-width cap, then wrapping and growing height.
Sized via a hidden mirror element (same font/padding, real DOM
measurement -- a <textarea> can't self-size to content with CSS alone).
2. F4 now cycles every reference a real text selection overlaps, each from
its own lock state, not just the one at the caret -- select a stretch of
formula spanning several references and F4 adjusts all of them at once,
same as Excel. cycleReferenceLock(formula, caret) -> cycleReferenceLocks
(formula, start, end); a collapsed selection (start === end) keeps the
old single-reference caret behaviour. Wired into both the in-cell editor
and the formula bar.
3. Alt+Enter inserts a literal newline in the editor instead of committing
(explicit splice at the caret, not relying on the textarea's native
Enter handling, which isn't consistently reachable for Alt+Enter). A
cell holding a newline displays it wrapped (.cell.multiline) instead of
collapsing to one ellipsised line.
Along the way: a <td>'s own max-height doesn't cap table-row growth in any
browser (a table-layout quirk) -- a wrapped multi-line cell was silently
growing its row past rowH(r), which would have desynced every pixel
position computed from rowH()/rowTop() (the overlays, sticky frozen panes,
the row-resize handle). Fixed by moving cell text into its own block-level
.cell-content child, which *does* respect max-height + overflow:hidden.
Verified interactively against the dev server with Playwright: a long
formula gets a ~300px editor instead of the 104px cell; Alt+Enter produces
a real "\n" that survives commit and displays wrapped, with the row height
unchanged (26px, not the ~40px it grew to before the .cell-content fix);
F4 on a full-formula selection turns every reference absolute in one press
and keeps cycling all of them on repeated presses; existing selection,
copy/paste and fill-handle behaviour all regression-checked clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>