Skip to content

[lexical-code-core][lexical-code-prism][lexical-code-shiki] Refactor: move the duplicated updateCodeGutter into @lexical/code-core - #9164

Open
Om-singhaI wants to merge 6 commits into
facebook:mainfrom
Om-singhaI:refactor/code-gutter-core
Open

Om-singhaI wants to merge 6 commits into
facebook:mainfrom
Om-singhaI:refactor/code-gutter-core

Conversation

@Om-singhaI

Copy link
Copy Markdown
Contributor

Description

@lexical/code-prism and @lexical/code-shiki each had their own private copy of updateCodeGutter, and the two copies were identical line for line. @etrepum asked on #8468 for this gutter code to be consolidated into @lexical/code-core, and it's the first step of the plan I posted on #8459.

This moves it into @lexical/code-core as $updateCodeGutter in a new CodeGutter.ts, exports it from the package index and declares it in the Flow file. Both highlighters now import it from there and their copies are gone. The function body is unchanged byte for byte, so the data-gutter string and the __cachedChildrenLength check are exactly what they were.

I gave it the $ prefix because it reads the node's children, so it has to run inside a read or an update. That's already where both callers use it: the CodeNode mutation listener, inside editor.read('latest', ...). It's marked @internal like registerCodeIndentation, since only the two highlighters call it.

I also added unit tests in @lexical/code-core that pin the exact data-gutter value for a single line, several lines, an empty trailing line and an edit that changes the line count, plus the cache that skips the write when the child count doesn't change. They run in a real editor with a copy of the mutation listener both highlighters register.

Part of #8459

Test plan

Before

On origin/main (84eabc3). The packages/lexical-code filter picks up lexical-code, lexical-code-core, lexical-code-prism and lexical-code-shiki. The lexical-code tests already pin data-gutter through prism's registerCodeHighlighting.

$ pnpm exec vitest --project unit --no-watch --maxWorkers=2 packages/lexical-code
 Test Files  15 passed (15)
      Tests  291 passed | 1 skipped (292)

After

$ pnpm exec vitest --project unit --no-watch --maxWorkers=2 --reporter=verbose packages/lexical-code
 ✓ |unit| packages/lexical-code-core/src/__tests__/unit/CodeGutter.test.ts > $updateCodeGutter > numbers a single line 108ms
 ✓ |unit| packages/lexical-code-core/src/__tests__/unit/CodeGutter.test.ts > $updateCodeGutter > numbers several lines 6ms
 ✓ |unit| packages/lexical-code-core/src/__tests__/unit/CodeGutter.test.ts > $updateCodeGutter > numbers an empty trailing line 6ms
 ✓ |unit| packages/lexical-code-core/src/__tests__/unit/CodeGutter.test.ts > $updateCodeGutter > follows an edit that changes the line count 7ms
 ✓ |unit| packages/lexical-code-core/src/__tests__/unit/CodeGutter.test.ts > $updateCodeGutter > only rewrites the attribute when the number of children changes 4ms
 Test Files  16 passed (16)
      Tests  296 passed | 1 skipped (297)

$ pnpm run flow
Found 0 errors

tsc over the four code packages passes, and eslint and prettier are clean on the changed files. I ran this on Node 25 locally, so Node 22 and 24 are left to CI. I didn't run the browser or e2e suites locally either. The playground e2e specs pin data-gutter in a lot of places, and since the output is unchanged they should stay green.

… move the duplicated updateCodeGutter into @lexical/code-core

## Description

`@lexical/code-prism` and `@lexical/code-shiki` each had their own private copy of `updateCodeGutter`, and the two copies were identical line for line. @etrepum asked on facebook#8468 for this gutter code to be consolidated into `@lexical/code-core`, and it's the first step of the plan I posted on facebook#8459.

This moves it into `@lexical/code-core` as `$updateCodeGutter` in a new `CodeGutter.ts`, exports it from the package index and declares it in the Flow file. Both highlighters now import it from there and their copies are gone. The function body is unchanged byte for byte, so the `data-gutter` string and the `__cachedChildrenLength` check are exactly what they were.

I gave it the `$` prefix because it reads the node's children, so it has to run inside a read or an update. That's already where both callers use it: the CodeNode mutation listener, inside `editor.read('latest', ...)`. It's marked `@internal` like `registerCodeIndentation`, since only the two highlighters call it.

I also added unit tests in `@lexical/code-core` that pin the exact `data-gutter` value for a single line, several lines, an empty trailing line and an edit that changes the line count, plus the cache that skips the write when the child count doesn't change. They run in a real editor with a copy of the mutation listener both highlighters register.

Part of facebook#8459

## Test plan

### Before

On `origin/main` (84eabc3). The `packages/lexical-code` filter picks up `lexical-code`, `lexical-code-core`, `lexical-code-prism` and `lexical-code-shiki`. The `lexical-code` tests already pin `data-gutter` through prism's `registerCodeHighlighting`.

```
$ pnpm exec vitest --project unit --no-watch --maxWorkers=2 packages/lexical-code
 Test Files  15 passed (15)
      Tests  291 passed | 1 skipped (292)
```

### After

```
$ pnpm exec vitest --project unit --no-watch --maxWorkers=2 --reporter=verbose packages/lexical-code
 ✓ |unit| packages/lexical-code-core/src/__tests__/unit/CodeGutter.test.ts > $updateCodeGutter > numbers a single line 108ms
 ✓ |unit| packages/lexical-code-core/src/__tests__/unit/CodeGutter.test.ts > $updateCodeGutter > numbers several lines 6ms
 ✓ |unit| packages/lexical-code-core/src/__tests__/unit/CodeGutter.test.ts > $updateCodeGutter > numbers an empty trailing line 6ms
 ✓ |unit| packages/lexical-code-core/src/__tests__/unit/CodeGutter.test.ts > $updateCodeGutter > follows an edit that changes the line count 7ms
 ✓ |unit| packages/lexical-code-core/src/__tests__/unit/CodeGutter.test.ts > $updateCodeGutter > only rewrites the attribute when the number of children changes 4ms
 Test Files  16 passed (16)
      Tests  296 passed | 1 skipped (297)

$ pnpm run flow
Found 0 errors
```

`tsc` over the four code packages passes, and eslint and prettier are clean on the changed files. I ran this on Node 25 locally, so Node 22 and 24 are left to CI. I didn't run the browser or e2e suites locally either. The playground e2e specs pin `data-gutter` in a lot of places, and since the output is unchanged they should stay green.
@vercel

vercel Bot commented Sep 13, 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 Sep 18, 2026 3:01am UTC
lexical-playground Ready Ready Preview Sep 18, 2026 3:01am UTC

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 Sep 13, 2026
@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Sep 13, 2026
@Om-singhaI

Copy link
Copy Markdown
Contributor Author

@etrepum I've got the next piece from #8459 ready, the per line numbers, opt in and stacked on this branch. Would you rather land this one first, or should I open it now so you can see both together?

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

The editor argument is redundant, $getEditor can be used in the implementation

The editor argument was redundant. Every call site already runs inside a read or update context, so the implementation can reach the editor itself.
@Om-singhaI

Copy link
Copy Markdown
Contributor Author

Done, thanks. Every call site already runs inside a read or an update context, so $getEditor() resolves there and the argument wasn't buying anything. The Flow declaration and both highlighters are updated to match, and the gutter tests and tsc are green.

Comment thread packages/lexical-code-shiki/src/CodeHighlighterShiki.ts Outdated
…utter

The mutation listener was copied into both highlighters and the tests. Moving it behind one registration function leaves the gutter update as an implementation detail.
registerCodeIndentation carries the tag and both are exported from the
package index, so without it the gutter registration would show up as
supported API in the generated docs, which strip internal entries.
Comment thread packages/lexical-code-core/src/CodeGutter.ts Outdated
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.

2 participants