Skip to content

[lexical-y] Bug Fix: Keep the text before the node being composed in a code token split by comment ids - #9580

Queued
zurfyx wants to merge 2 commits into
mainfrom
zurfyx/y-compose-code-token-segment
Queued

zurfyx wants to merge 2 commits into
mainfrom
zurfyx/y-compose-code-token-segment

Conversation

@zurfyx

@zurfyx zurfyx commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Description

While an editor composes with an IME, a read of the line being composed keeps the composed node and its key, so the browser goes on composing in its DOM text node. The read reuses the line's other text nodes for the other pieces: in order, or by segment for code highlight nodes (the nodes of one highlighted token share a segment).

A code token that NodeState splits into several highlight nodes (comment ids on part of it) shares one segment among them, and the reader's segment lookup kept the last of them: in the reported line, the node being composed. When a collaborator's change to the line made the editor read it during the composition, an earlier piece of that token took the composed node by segment and set its text, and the composed piece then took the same node again. The earlier piece's text (// t46z note t47z, which nobody edited) disappeared from the editor, and the composing client wrote the line without it for every client when the composition ended. The guard against reusing the composed node covered only the in-order reuse.

Now, once the composed node is reserved for its piece, it counts as used: no other piece reuses it, in order or by segment.

Closes #9577

Changeset

.changeset/lexical-y-composition-code-segments.md (@lexical/y, [fix]).

Test plan

regressions/CompositionCodeSegments.test.ts replays the issue's captured document and the two remote updates, composing and not composing.

pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/regressions/CompositionCodeSegments.test.ts

Before

     × remote comment id changes to the line keep its text (composing: true) 126ms
     ✓ remote comment id changes to the line keep its text (composing: false) 49ms
Expected: "const t44z = fetch(t45z); // t46z note t47zt48z t49z t50z t t51z"
Received: "const t44z = fetch(t45z); t48z t49z t50z t t51z"
      Tests  1 failed | 1 passed (2)

After

The second commit also checks that the node being composed stays attached after each remote update (Tests 2 passed (2)).

 ✓  unit  packages/lexical-y/src/__tests__/unit/regressions/CompositionCodeSegments.test.ts (2 tests) 235ms
      Tests  2 passed (2)

pnpm exec vitest run --project unit packages/lexical-y packages/lexical-code-prism packages/lexical-mark:

 Test Files  188 passed | 2 skipped (190)
      Tests  8765 passed | 46 expected fail | 4 skipped (8815)

pnpm run ci-check (each script): tsc, tsc-test, tsc-scripts, tsc-extension, tsc-website, flow, prettier, lint, check-changesets pass.

…a code token split by comment ids

## Description

While an editor composes with an IME, a read of the line being composed keeps the composed node and its key, so the browser goes on composing in its DOM text node. The read reuses the line's other text nodes for the other pieces: in order, or by segment for code highlight nodes (the nodes of one highlighted token share a segment).

A code token that NodeState splits into several highlight nodes (comment ids on part of it) shares one segment among them, and the reader's segment lookup kept the last of them: in the reported line, the node being composed. When a collaborator's change to the line made the editor read it during the composition, an earlier piece of that token took the composed node by segment and set its text, and the composed piece then took the same node again. The earlier piece's text (`// t46z note t47z`, which nobody edited) disappeared from the editor, and the composing client wrote the line without it for every client when the composition ended. The guard against reusing the composed node covered only the in-order reuse.

Now, once the composed node is reserved for its piece, it counts as used: no other piece reuses it, in order or by segment. The same reuse also lost text in a second line shape found by the soak (a remote insert in a code line before the composed token), which this fixes too.

Closes #9577

## Changeset

`.changeset/lexical-y-composition-code-segments.md` (`@lexical/y`, `[fix]`).

## Test plan

`regressions/CompositionCodeSegments.test.ts` replays the issue's captured document and the two remote updates, composing and not composing.

```
pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/regressions/CompositionCodeSegments.test.ts
```

### Before

```
     × remote comment id changes to the line keep its text (composing: true) 126ms
     ✓ remote comment id changes to the line keep its text (composing: false) 49ms
Expected: "const t44z = fetch(t45z); // t46z note t47zt48z t49z t50z t t51z"
Received: "const t44z = fetch(t45z); t48z t49z t50z t t51z"
      Tests  1 failed | 1 passed (2)
```

### After

```
 ✓  unit  packages/lexical-y/src/__tests__/unit/regressions/CompositionCodeSegments.test.ts (2 tests) 235ms
      Tests  2 passed (2)
```

`pnpm exec vitest run --project unit packages/lexical-y packages/lexical-code-prism packages/lexical-mark`:

```
 Test Files  188 passed | 2 skipped (190)
      Tests  8765 passed | 46 expected fail | 4 skipped (8815)
```

`pnpm run ci-check` (each script): tsc, tsc-test, tsc-scripts, tsc-extension, tsc-website, flow, prettier, lint, check-changesets pass.
@vercel

vercel Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
lexical Ready Ready Preview Oct 30, 2026 1:46pm UTC
lexical-playground Ready Ready Preview Oct 30, 2026 1:46pm UTC

Request Review

@zurfyx
zurfyx requested a review from etrepum as a code owner October 30, 2026 12:19
@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 Oct 30, 2026
@meta-code-review

meta-code-review Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🔄 In progress (for maintainers only) Oct 30, 2026, 1:51 PM UTC

@pkg-pr-new

pkg-pr-new Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
lexical

npm i https://pkg.pr.new/lexical@9580

@lexical/a11y

npm i https://pkg.pr.new/@lexical/a11y@9580

@lexical/clipboard

npm i https://pkg.pr.new/@lexical/clipboard@9580

@lexical/code

npm i https://pkg.pr.new/@lexical/code@9580

@lexical/code-core

npm i https://pkg.pr.new/@lexical/code-core@9580

@lexical/code-prism

npm i https://pkg.pr.new/@lexical/code-prism@9580

@lexical/code-shiki

npm i https://pkg.pr.new/@lexical/code-shiki@9580

@lexical/compiler

npm i https://pkg.pr.new/@lexical/compiler@9580

@lexical/devtools-core

npm i https://pkg.pr.new/@lexical/devtools-core@9580

@lexical/dragon

npm i https://pkg.pr.new/@lexical/dragon@9580

@lexical/eslint-plugin

npm i https://pkg.pr.new/@lexical/eslint-plugin@9580

@lexical/extension

npm i https://pkg.pr.new/@lexical/extension@9580

@lexical/fast-check

npm i https://pkg.pr.new/@lexical/fast-check@9580

@lexical/file

npm i https://pkg.pr.new/@lexical/file@9580

@lexical/hashtag

npm i https://pkg.pr.new/@lexical/hashtag@9580

@lexical/headless

npm i https://pkg.pr.new/@lexical/headless@9580

@lexical/history

npm i https://pkg.pr.new/@lexical/history@9580

@lexical/html

npm i https://pkg.pr.new/@lexical/html@9580

@lexical/internal

npm i https://pkg.pr.new/@lexical/internal@9580

@lexical/link

npm i https://pkg.pr.new/@lexical/link@9580

@lexical/list

npm i https://pkg.pr.new/@lexical/list@9580

@lexical/mark

npm i https://pkg.pr.new/@lexical/mark@9580

@lexical/markdown

npm i https://pkg.pr.new/@lexical/markdown@9580

@lexical/mdast

npm i https://pkg.pr.new/@lexical/mdast@9580

@lexical/offset

npm i https://pkg.pr.new/@lexical/offset@9580

@lexical/overflow

npm i https://pkg.pr.new/@lexical/overflow@9580

@lexical/plain-text

npm i https://pkg.pr.new/@lexical/plain-text@9580

@lexical/react

npm i https://pkg.pr.new/@lexical/react@9580

@lexical/rich-text

npm i https://pkg.pr.new/@lexical/rich-text@9580

@lexical/selection

npm i https://pkg.pr.new/@lexical/selection@9580

@lexical/table

npm i https://pkg.pr.new/@lexical/table@9580

@lexical/tailwind

npm i https://pkg.pr.new/@lexical/tailwind@9580

@lexical/text

npm i https://pkg.pr.new/@lexical/text@9580

@lexical/utils

npm i https://pkg.pr.new/@lexical/utils@9580

@lexical/y

npm i https://pkg.pr.new/@lexical/y@9580

@lexical/yjs

npm i https://pkg.pr.new/@lexical/yjs@9580

commit: c00aea6

@meta-code-review meta-code-review 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.

Thanks for tracking this down with a captured replay. Marking the composed node as used once it is placed (packages/lexical-y/src/internal/Read.ts, the new block after the two placement loops) closes the path the old in-order-only guard missed. bySegment keeps only the last node of each segment, so an earlier piece of a code token that NodeState split could take the composed node; the composed piece then took the same node again and overwrote that text. Without the Read.ts change, the new test's composing case fails with Received: "const t44z = fetch(t45z); t48z t49z t50z t t51z". With it, both cases pass, and the composed node keeps its key and caret through both remote updates. The changeset follows CONTRIBUTING.md: a patch bump, a one-line [fix] summary and a Verify: paragraph.

Two suggestions:

  1. Back the second line shape with a test, or drop the claim. The description says this also fixes "a remote insert in a code line before the composed token", but CompositionCodeSegments.test.ts replays only #9577's document and its two comment-id updates. AGENTS.md asks the Test plan to carry the real Before and After output of what was run. Please add that captured shape as a second case, or remove the sentence.
  2. Nit: assert that the composition stays in its node (CompositionCodeSegments.test.ts:154). Without the fix, the second remote update also detaches the composed node and moves the composition key to a new node, which in a browser replaces the DOM text node the IME is composing in. The test compares line text only. Checking that $getNodeByKey(key) is still attached after each update would pin that behavior as well.

Optional: the extended workflow, which includes the Collab Y e2e specs, was skipped on this head because its gate (the extended-tests label or an approving review) was closed. A maintainer can add the label to get browser coverage of the collab path.

for (const u of REMOTE) {
Y.applyUpdate(doc, b64(u), 'remote');
await idle();
expect(codeLine(editor)).toBe(expected);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: this compares line text only. Without the Read.ts change, the second update also detaches the composed node and moves the composition key to a new node, which in a browser replaces the DOM text node the IME is composing in. An assertion like this would pin that the composition stays in its node:

expect(
  editor.getEditorState().read(() => $getNodeByKey(key)?.isAttached() ?? false),
).toBe(composing);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in c00aea6: after each remote update the test now checks that the composed node is still attached (toBe(composing), as suggested).

…#9577 test

## Description

The #9577 regression test compared only the code line's text. It now also checks, after each remote update, that the node being composed is still attached: before the fix, the second update also detached it and moved the composition to a new node, which in a browser replaces the DOM text node the IME is composing in.

## Test plan

```
pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/regressions/CompositionCodeSegments.test.ts
```

### Before

Without the `Read.ts` change:

```
AssertionError: expected 'const t44z = fetch(t45z); t48z t49z t…' to be 'const t44z = fetch(t45z); // t46z not…' // Object.is equality
Expected: "const t44z = fetch(t45z); // t46z note t47zt48z t49z t50z t t51z"
Received: "const t44z = fetch(t45z); t48z t49z t50z t t51z"
```

### After

```
      Tests  2 passed (2)
```
@zurfyx

zurfyx commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Review follow-ups: (1) the second line shape needs the randomized harness it was captured with, so I removed that sentence from the description rather than add a test that depends on the harness; (2) the composition check is in c00aea6.

This branch was successfully deployed

2 active deployments
Preview – lexical — c00aea64 Deployed Oct 30, 2026 by vercel[bot]
Preview – lexical-playground — c00aea64 Deployed Oct 30, 2026 by vercel[bot]
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.

Projects

None yet

1 participant