Skip to content

feat: rtl mixed bidi#3320

Open
artem-harbour wants to merge 4 commits into
mainfrom
artem/rtl-v12
Open

feat: rtl mixed bidi#3320
artem-harbour wants to merge 4 commits into
mainfrom
artem/rtl-v12

Conversation

@artem-harbour
Copy link
Copy Markdown
Contributor

@artem-harbour artem-harbour commented May 15, 2026

Linear: SD-2933

This PR closes the mixed-bidi hardening milestone for RTL editing.

Included:

  • Renderer update for per-run dir assignment in mixed RTL/LTR content to improve token order stability.
  • Caret geometry hardening at bidi boundaries (native selection fallback + scoped DOM probing) to reduce caret drift/snap issues.
  • New mixed-bidi-backspace extension for visual-left deletion at RTL/LTR boundaries with fail-open behavior.
  • Expanded unit and behavior coverage for mixed-bidi click, arrow navigation, selection, and backspace scenarios.

Result:

  • Improves mixed-bidi typing/caret/deletion reliability in body and header/footer surfaces.
  • Preserves non-bidi flows via conservative guards and fallback behavior.
  • Keeps scope focused on production-usable stability for current fixtures.

Notes

  • Mixed-bidi still has corner cases.
  • Recommend deferring deeper investigation/fixes until real reports arrive, and then handling them through a normal bug queue.
  • Reason: mixed-bidi fixes are often high-risk and can regress unrelated editor behavior.
  • Also, mixed-bidi behavior in Word itself is frequently inconsistent/unpredictable across edge scenarios.

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@caio-pizzol caio-pizzol self-assigned this May 15, 2026
@artem-harbour artem-harbour marked this pull request as ready for review May 15, 2026 17:12
@artem-harbour artem-harbour requested a review from a team as a code owner May 15, 2026 17:12
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 15, 2026

SD-2933

@artem-harbour artem-harbour requested a review from caio-pizzol May 15, 2026 17:12
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8ce8602149

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +144 to +145
const tr = state.tr.delete(boundary.left.pmStart, boundary.left.pmEnd).scrollIntoView();
view.dispatch(tr);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve Backspace undo boundaries for mixed bidi

When this shortcut handles Backspace at a mixed-direction boundary, it returns before the normal Backspace keymap runs, but this direct tr.delete(...) skips the dispatchHistoryBoundary(view) call and inputType meta that core/extensions/keymap.js applies for all other Backspace deletes. In that scenario, the deletion can be merged into the preceding typing undo group instead of becoming its own undo step, so undo may remove both the prior edit and this boundary deletion. Add the same history boundary/input metadata here or delegate through the existing Backspace command path after resolving the target range.

Useful? React with 👍 / 👎.

…space chain

The MixedBidiBackspace extension previously bound Backspace via addShortcuts
and dispatched its own transaction. That bypassed the canonical handleBackspace
chain in core/extensions/keymap.js, skipping:

- dispatchHistoryBoundary (so undo grouping diverged from regular Backspace),
- the tr.setMeta('inputType', 'deleteContentBackward') hop (tracked-changes
  helpers gate Backspace-specific wrapping on this meta - see
  trackChangesHelpers/trackedTransaction.js:447,
  trackChangesHelpers/replaceAroundStep.js:162),
- deleteBlockSdtAtTextBlockStart for SDT block boundaries,
- the rest of the specialized run-aware ladder.

Refactor:

- Expose mixedBidiBackspace as a chain command via addCommands(); shape
  matches the existing backspaceAcrossRuns / backspaceNextToRun pattern
  (factory returning ({state, view, tr, dispatch}) => boolean).
- Insert into the Backspace chain in keymap.js after backspaceAcrossRuns
  (specialized cross-run handling) and before deleteSelection (generic
  fallback), guarded with `?? false` so the chain works when the extension
  is unregistered.
- Split the detection logic into pure resolveMixedBidiBackspaceRange so the
  command body stays tiny.
- Tests rewritten to call the chain command shape directly and pin: chain
  dry-run mode (dispatch=undefined) returns true without mutating tr,
  non-mixed boundaries fall through, both RTL+LTR and LTR+RTL boundaries
  handled, posAtDOM is skipped early for pure-LTR/RTL lines.
…ine-direction

The shouldAssignPerRunRtlDir / normalizeRtlDateTokenForWordParity helpers
and their regexes (RTL_DATE_LIKE_TOKEN_RE, STRONG_RTL_CHAR_RE,
LATIN_DIGIT_NEUTRAL_ONLY_RE) lived at the bottom of renderer.ts. After
#3307 moved the rtl-paragraph feature folder to inline-direction with an
explicit axis scope, the renderer was the wrong home: these helpers are
paint-time decisions about how to project w:rPr/w:rtl onto a rendered
span's dir attribute, which is exactly what features/inline-direction
owns.

Extract into features/inline-direction/run-direction.ts:

- Combine the two-step decision (set dir=rtl? else set dir=ltr for
  date-like?) into a single resolveRunDirectionAttribute helper that
  returns 'rtl' | 'ltr' | null.
- Expose normalizeRtlDateTokenForWordParity alongside since it shares
  RTL_DATE_LIKE_TOKEN_RE.
- Inline the decision table as JSDoc, explicitly scoping the heuristic
  to current SD-3098 fixtures and pointing at the spec sections plus the
  known follow-up gaps (w:dir, w:bdo, w:lang/@bidi numeric, presentation
  forms).

Renderer collapses the per-span direction logic to one helper call.
22 new unit tests in run-direction.test.ts cover both branches of the
rtl-tagged decision table, the date-like ltr fallback for non-tagged
runs, and the regex coverage smoke tests.
… paint directive

Per §17.3.2.30, w:rPr/w:rtl does two things at the model level: forces
the complex-script formatting stack, and acts as a directionality
override for weak/neutral characters (NOT a forced visual flip of
strong-LTR text - §17.3.2.30 explicitly says behavior on strong-LTR is
unspecified).

Update the JSDoc on RunBidiContext.rtl to make explicit:

- rtl: true is the source signal (w:rPr/w:rtl was set in OOXML)
- It is NOT a directive that every consumer must project to dir="rtl"
  in the rendered DOM
- The painter decides the DOM projection per its Word-parity rules
  (resolveRunDirectionAttribute in features/inline-direction)
- Exporters must preserve rtl: true on round-trip regardless of paint
  decisions, since dropping it would lose source semantics

Doc-only change. No code or type signature changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants