Skip to content

[lexical-markdown] Bug Fix: Preserve inline formatting when wrapping already-formatted text with matching markers#8728

Open
koki-develop wants to merge 1 commit into
facebook:mainfrom
koki-develop:fix/markdown-shortcut-wrap-preserves-format
Open

[lexical-markdown] Bug Fix: Preserve inline formatting when wrapping already-formatted text with matching markers#8728
koki-develop wants to merge 1 commit into
facebook:mainfrom
koki-develop:fix/markdown-shortcut-wrap-preserves-format

Conversation

@koki-develop

Copy link
Copy Markdown

Closes #8727

Description

The apply-formatting loop in $runTextFormatTransformers (after the markers are stripped) had two issues:

  1. The nextSelection.hasFormat(format) guard was a no-op. nextSelection is a freshly-created $createRangeSelection() whose format is initialised to 0, so the check always returned false regardless of the actual node format.
  2. nextSelection.formatText(format) was called with alignWithFormat = null, which XOR-toggles the bit. For nodes that already had the format, this cleared it.

Replaced the loop with a per-node "set ON, never toggle" pass over nextSelection.extract(): for each node, set the bit only if hasFormat(format) is false. This mirrors the existing pattern used on the import side (https://github.com/facebook/lexical/blob/main/packages/lexical-markdown/src/importTextFormatTransformer.ts#L455-L461).

Test plan

Before

Reproduces via the Steps To Reproduce in issue #8727, or via the minimal reproduction environment:

After

Verified in the Lexical playground that every inline format marker (**, __, *, _, ~~, ==, ***, ___) preserves the formatting when wrapping already-formatted text.

…already-formatted text with matching markers

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment Jun 20, 2026 10:55am
lexical-playground Ready Ready Preview, Comment Jun 20, 2026 10:55am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 20, 2026

@potatowagon potatowagon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed by Navi (Tater Thoughts Bobblehead) on behalf of @potatowagon.

Assessment: LGTM ✅ — Clean, well-motivated bug fix.

What I checked:

Logic correctness: The old code had two clear bugs as described in the PR:

  1. nextSelection.hasFormat(format) was checking the selection format (initialized to 0), not the node format — so it always returned false (no-op guard).
  2. nextSelection.formatText(format) XOR-toggles the bit, so already-formatted nodes got their format cleared instead of preserved.

The fix correctly iterates over extracted nodes and only toggles the format ON when the node does not already have it. This is a "set ON, never toggle OFF" pattern — exactly right for "wrapping already-formatted text."

Edge cases considered:

  • Mixed-format selections (some nodes bold, some not): ✅ Only unformatted nodes get toggled.
  • Non-text nodes in selection: ✅ $isTextNode(node) guard skips them.
  • Multiple formats (e.g. *** = bold+italic): ✅ Outer loop iterates matcher.format, inner loop applies per-node — each format applied independently.

Pattern consistency: The PR description notes this mirrors the import-side logic at importTextFormatTransformer.ts#L455-461. I confirmed the same per-node hasFormat/toggleFormat pattern is used there.

Test coverage: No new automated tests added, but the fix is minimal (5 lines changed) and the author verified all inline format markers (**, __, *, _, ~~, ==, ***, ___) in the playground. Given the simplicity of the fix and the clear bug reproduction in #8727, manual verification is adequate here.

CI status: All checks green (CLA ✅, Vercel previews ✅). Note: no unit/e2e CI triggered yet — only CLA and deploy previews ran. The full test suite hasn't run, but this is a one-file, 5-line change with no API surface changes.

www compat: No exports changed, no API surface modified, no type signature changes. Internal consumers (MLCComposer, EPS) are unaffected — this only changes runtime behavior of the markdown shortcut transformer to stop incorrectly clearing format bits.

Risk: Very low. The change is strictly additive in effect (preserves formatting that was previously incorrectly removed). No regressions expected.

@mayrang

mayrang commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Nice issue write-up — the reproduction steps and screenshots made it easy to confirm the bug.

A regression test would strengthen this fix. One gotcha with headless tests: insertText('*') on an already-formatted node doesn't reliably trigger the markdown shortcut. Using setTextContent to build the full marker text (e.g. **hello**) and then select to place the cursor at the end works. You'll also need a flush update (editor.update(() => {}, {discrete: true})) afterward to let the queued markdown listener run.

I also left an inline suggestion for a simpler approach to the fix itself.

nextSelection.focus.set(closeNode.__key, newOffset, 'text');

// Apply formatting to selected text
const extractedNodes = nextSelection.extract();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

RangeSelection.formatText already has a second alignWithFormat parameter designed for this — it applies the format without toggling it off on nodes that already have it. This avoids reimplementing the split-and-check logic that formatText handles internally:

import {TEXT_TYPE_TO_FORMAT} from 'lexical';

for (const format of matcher.format) {
  nextSelection.formatText(format, TEXT_TYPE_TO_FORMAT[format]);
}

@etrepum etrepum 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.

This should have tests to confirm that it behaves as expected

@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: [lexical-markdown] Wrapping already-formatted text with the matching inline markers removes the formatting

4 participants