Skip to content

Preserve absolute reference ($) markers in formula operations#22

Merged
hackerwins merged 1 commit into
wafflebase:mainfrom
ggyuchive:apply-fix-char
Mar 5, 2026
Merged

Preserve absolute reference ($) markers in formula operations#22
hackerwins merged 1 commit into
wafflebase:mainfrom
ggyuchive:apply-fix-char

Conversation

@ggyuchive

@ggyuchive ggyuchive commented Mar 5, 2026

Copy link
Copy Markdown
Member

Summary

Add ARef type and parseARef/toASref helpers to preserve $ flags through all formula rewriting paths (shift, relocate, redirect, move).

Why

Previously, shifting, relocating, and redirecting formulas would strip the $ markers from cell references like $A$1, $A1, or A$1. This caused incorrect behavior when copy-pasting or inserting rows/columns in formulas that used absolute references.

Linked Issues

Fixes #

Verification

CI automatically posts a verification summary comment on this PR with
per-lane results for both verify:self and verify:integration.

  • verify:self — CI comment shows ✅
  • verify:integration — CI comment shows ✅ (or explicit skip reason below)

Skip reason (if applicable):

Risk Assessment

  • User-facing risk:
  • Data/security risk:
  • Rollback plan:

Notes for Reviewers

  • UI changes (screenshots/gifs if applicable):
  • Follow-up work (if any):

Summary by CodeRabbit

Release Notes

  • New Features

    • Strengthened absolute cell reference ($) handling in formulas, ensuring proper preservation when formulas are shifted, moved, or relocated across cells.
    • Enhanced support for various absolute reference patterns, including column-only and row-only absolute markers.
  • Tests

    • Comprehensive test coverage added for absolute reference behavior, including cross-sheet operations and edge cases.

Previously, shifting, relocating, and redirecting formulas would strip
the $ markers from cell references like $A$1, $A1, or A$1. This caused
incorrect behavior when copy-pasting or inserting rows/columns in
formulas that used absolute references.

Add ARef type and parseARef/toASref helpers to preserve $ flags through
all formula rewriting paths (shift, relocate, redirect, move).

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@coderabbitai

coderabbitai Bot commented Mar 5, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The changes add support for absolute cell reference handling ($-prefixed) in formula shifting operations. A new ARef type extends Ref with optional absolute column/row markers, accompanied by parsing and serialization utilities, with core shifting logic refactored to preserve these markers during formula relocation and redirection.

Changes

Cohort / File(s) Summary
Type System & Parsing Utilities
packages/sheet/src/model/types.ts, packages/sheet/src/model/coordinates.ts
New ARef type extends Ref with absCol and absRow flags. Added parseARef() and toASref() functions to parse and serialize absolute reference markers ($) in cell references.
Core Formula Shifting Logic
packages/sheet/src/model/shifting.ts
Reworked formula handling to operate on ARef objects internally, preserving absolute markers throughout shifting, relocation, and redirection. Updated moveFormula, shiftFormula, and redirectFormula. Added helpers: relocateARef, applyAbsMarkers, shiftARef.
Test Coverage
packages/sheet/test/sheet/shifting.test.ts
Added comprehensive test suite for absolute reference handling in formula operations, covering fully absolute, column/row-only, mixed references, ranges, and edge cases. Exposed redirectFormula as public API.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 With dollar signs we mark the way,
Absolute refs that stay, oh stay!
Through shifts and moves they hold their ground,
A steadfast anchor, safe and sound!
Hopping formulas, no more astray! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main objective of the pull request: preserving absolute reference markers ($) across formula operations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/sheet/src/model/coordinates.ts (1)

345-360: Unreachable code path in absCol detection.

The ref.includes('!$') check on line 346 appears to be dead code. The regex on line 349 (^(\$?)([A-Za-z]+)(\$?)(\d+)$) will throw an error for any cross-sheet reference containing !, so this condition can never be true for a valid input that passes the regex check.

Looking at the call sites in shifting.ts, parseARef is always called on local references after the cross-sheet prefix has already been stripped (e.g., text.toUpperCase() where text excludes the sheet prefix).

🔧 Suggested simplification
 export function parseARef(ref: Sref): ARef {
-  const absCol = ref.startsWith('$') || ref.includes('!$');
+  const absCol = ref.startsWith('$');
   // Find the column-row boundary to detect row dollar sign.
   // After optional leading '$' and column letters, check for '$' before digits.
   const match = ref.match(/^(\$?)([A-Za-z]+)(\$?)(\d+)$/);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheet/src/model/coordinates.ts` around lines 345 - 360, The absCol
detection in parseARef incorrectly checks ref.includes('!$'), which is
unreachable because the regex /^(\$?)([A-Za-z]+)(\$?)(\d+)$/ used later rejects
any string containing '!', so remove the dead includes('!$') branch and compute
absCol solely from ref.startsWith('$'); update the attendant comment to state
that the code assumes any sheet prefix has been stripped before calling
parseARef and keep the existing regex-based validation and absRow handling
(match[3] === '$') intact.
packages/sheet/src/model/shifting.ts (1)

80-88: Consider extracting repeated abs marker spreading pattern.

The pattern for preserving absolute markers is repeated verbosely here and in several other places (shiftSref, shiftARef, relocateARef):

{ ...moved, ...(aref.absCol ? { absCol: true } : {}), ...(aref.absRow ? { absRow: true } : {}) }

This works correctly, but a small helper could reduce duplication and improve readability.

🔧 Optional helper to reduce repetition
/** Copies absCol/absRow flags from source ARef to a Ref */
function withAbsMarkers(ref: Ref, source: ARef): ARef {
  return {
    ...ref,
    ...(source.absCol ? { absCol: true } : {}),
    ...(source.absRow ? { absRow: true } : {}),
  };
}

Then usage becomes:

-result += toASref({ ...moved, ...(aref.absCol ? { absCol: true } : {}), ...(aref.absRow ? { absRow: true } : {}) });
+result += toASref(withAbsMarkers(moved, aref));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sheet/src/model/shifting.ts` around lines 80 - 88, The repeated
pattern that re-applies absolute markers ({ ...(aref.absCol ? { absCol: true } :
{}), ...(aref.absRow ? { absRow: true } : {}) }) should be extracted into a
small helper to reduce duplication and improve readability; add a function
(e.g., withAbsMarkers(ref: Ref, source: ARef): ARef) that returns { ...ref,
...(source.absCol ? { absCol: true } : {}), ...(source.absRow ? { absRow: true }
: {}) } and replace occurrences in this file where parseARef, moveRef and
toASref are combined (and similar sites in shiftSref, shiftARef, relocateARef)
to call withAbsMarkers(movedOrNewRef, originalARef) before passing to toASref.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/sheet/src/model/coordinates.ts`:
- Around line 345-360: The absCol detection in parseARef incorrectly checks
ref.includes('!$'), which is unreachable because the regex
/^(\$?)([A-Za-z]+)(\$?)(\d+)$/ used later rejects any string containing '!', so
remove the dead includes('!$') branch and compute absCol solely from
ref.startsWith('$'); update the attendant comment to state that the code assumes
any sheet prefix has been stripped before calling parseARef and keep the
existing regex-based validation and absRow handling (match[3] === '$') intact.

In `@packages/sheet/src/model/shifting.ts`:
- Around line 80-88: The repeated pattern that re-applies absolute markers ({
...(aref.absCol ? { absCol: true } : {}), ...(aref.absRow ? { absRow: true } :
{}) }) should be extracted into a small helper to reduce duplication and improve
readability; add a function (e.g., withAbsMarkers(ref: Ref, source: ARef): ARef)
that returns { ...ref, ...(source.absCol ? { absCol: true } : {}),
...(source.absRow ? { absRow: true } : {}) } and replace occurrences in this
file where parseARef, moveRef and toASref are combined (and similar sites in
shiftSref, shiftARef, relocateARef) to call withAbsMarkers(movedOrNewRef,
originalARef) before passing to toASref.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2da6a9aa-cd32-40a2-b360-117ee1d27150

📥 Commits

Reviewing files that changed from the base of the PR and between f63fe21 and 06bf0f8.

📒 Files selected for processing (4)
  • packages/sheet/src/model/coordinates.ts
  • packages/sheet/src/model/shifting.ts
  • packages/sheet/src/model/types.ts
  • packages/sheet/test/sheet/shifting.test.ts

@hackerwins
hackerwins self-requested a review March 5, 2026 19:50

@hackerwins hackerwins left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution.

@hackerwins
hackerwins merged commit 28f7f59 into wafflebase:main Mar 5, 2026
2 checks passed
hackerwins added a commit that referenced this pull request Jun 3, 2026
* Slides: text-edit + adjust drag entry on grouped elements (#22, #31)

Two view-layer paths assumed `slide.elements` is flat:

- `enterEditMode` resolved the click target via
  `Array.prototype.find`, so double-click on text/shape elements
  nested inside a group silently no-op'd. Slide 22 of the
  Yorkie deck (text "웹/모바일..." in the right column) reproduced
  this — `Selection.doubleClick` drilled in correctly but the
  mount path bailed.

- `startAdjustmentDrag` used the same flat `find`, so the yellow
  adjustment diamond on a grouped shape armed but never wrote.
  Slide 31's Dogfooding pentagonArrow reproduced this.

The render-side mask for the editing element and the live
preview for adjustments had the matching flat `slide.elements.map`
bug — even if the entry paths were fixed in isolation, the
canvas would still ghost-paint the element under the in-place
editor / miss the live preview for grouped shapes.

Both paths now resolve elements via the existing recursive
helpers (`findElement`, `buildElementWorldLookup`) and pass
world-frame elements where downstream code expects world coords
(overlay text-box mount, world↔local adjustment conversion).
The store-side mutation APIs (`requireElement`,
`updateElementFrame`, `updateElementData`, `withTextElement`,
`withShapeText`) already walk the tree via `findElementPath`,
so the writes work transparently.

Live-verified on slide 22 (text edit enters at the correct
world position, no canvas ghost) and slide 31 (Dogfooding
arrow's adjustments commit after drag).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* Slides: skip autofit-grow write when ancestor group is transformed

Code review caught a silent miswrite hole in the prior commit:
text-edit autofit-grow at commit time writes the editor-reported
content height (world / canvas-logical coords) straight into
`frame.h` via `store.updateElementFrame`, which expects LOCAL
coords. For top-level elements local === world; for groups with
rotation 0 + unit scale, w/h/rotation also match. But for a
rotated or non-unit-scale group, the world height would be stored
as local height, scrambling the element's frame.

Detect transformed ancestor by comparing local vs world frame
(w/h/rotation) at enter time and skip the autofit-grow write when
they diverge. Properly composing the inverse ancestor transform is
the right long-term fix, but until then this guard prevents the
silent corruption. Scope is unchanged: slide 22's groups have
rotation 0 + refSize === frame, so the autofit-grow path still
runs there.

Also annotate the todo's out-of-scope section with the matching
`startConnectorEndpointDrag` flat-find pattern (editor.ts:3135)
so future audits don't miss it. PPTX import can produce
connectors inside groups; the editor-side group op forbids it, so
the user-reachable failure mode is import-only — deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants