Skip to content

[lexical-code][lexical-code-shiki][lexical-markdown][lexical-playground][lexical-devtools] Feature: Experimental Shiki support for code highlighting#7662

Merged
etrepum merged 24 commits intofacebook:mainfrom
jeromew:code-shiki
Jul 18, 2025

Conversation

@jeromew
Copy link
Copy Markdown
Contributor

@jeromew jeromew commented Jul 1, 2025

This PR creates a new lexical-code-shiki package that adds support for the Shiki highlighting engine.

By default, [lexical-playground] still uses the Prism highlighting engine in [lexical-code]. New setting options in th playground make it possible to enable Shiki highlighting.

[lexical-code-shiki] implements dynamic support for all languages and themes supported by Shiki.

Prism support can be considered backward compatible except for one minor aspect enabling CodeBlocks to have dynamic loading of languages (see Breaking Changes below). This affects a small number of tests in [lexical-code] and [lexical-markdown].

The support is considered experimental in the sense that more work is expected on it before shiki support should be considered backwards compatible. Some things are still subject to change such as:

  • Build time preloading of syntax and theme modules
  • Restructuring shiki highlighted code blocks to be line-oriented (and having a gutter implementation for it)
  • Extracting prism to its own optional package (breaking change)

Breaking Changes

CodeNow now applies the data-highlight-language DOM attribute after highlighting, rather than immediately. The data-language attribute behavior is unchanged. This post-highlighting setting also applies to getIsSyntaxHighlightSupported() and its underlying __isSyntaxHighlightSupported property. Previously this would have been set eagerly because all known Prism languages were known and loaded at build time, now it represents whether the syntax highlighting has occurred or not.

Tests

All tests related to Prism have been left unmodified except those mentioned related to the Breaking changes.
Tests have been added to Shiki to control that all the Tab/Indent/Outdent cases pass.
Manual tests have been applied, but other Shiki tests that imply the tokenization of the code have been postponed until the restructuring of shiki highlighted code blocks to be line-oriented, as all the expected html snippets will need updating. These Shiki tests will be written by taking all the Prism tests as a starting point.
The Prism tests themselves will be kept as-is.

@vercel
Copy link
Copy Markdown

vercel bot commented Jul 1, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
lexical ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jul 14, 2025 9:44pm
lexical-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jul 14, 2025 9:44pm

@facebook-github-bot facebook-github-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 Jul 1, 2025
@jeromew jeromew changed the title [lexical-code][lexical-playground] Refactor lexical-code to add Shiki support with flat structure [lexical-code][lexical-playground] Refactor lexical-code to add Shiki support Jul 1, 2025
Copy link
Copy Markdown
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

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

some initial comments, did a quick look but not very thorough

if (languageInfo) {
// in case we arrive here concurrently (not yet loaded language is loaded twice)
// shiki's synchronous checks make sure to load it only once
await shiki.loadLanguage(languageInfo.import());
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.

using an await will force this to get deferred to the next microtask and cause some extra rendering, there should really be something that will use promises only when loading is in flight. similar to how promises work with react suspense.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I am not sure I get the full extent of your comment. I removed the await and replaced it with
shiki.loadLanguage(languageInfo.import()).then(..)
or do you mean languageInfo.import().then((module)=>shiki.loadLanguage(module); ..) ?

What I found about react suspense is

React catches that promise, pauses rendering, and shows the fallback UI you provided to . When the promise resolves, React tries rendering again. This is possible because in JavaScript, you can synchronously stop a function by throwing. React leverages this to "pause" rendering until the data is ready.

can you clarify what you mean by "there should really be something that will use promises only when loading is in flight. similar to how promises work with react suspense." ?

currently, CodeNodeTransform

  • fire-and-forget the loadAsset(language or theme) when they're not loaded + bails out if either needed loading. After loading, codeNode is marked as dirty

I could also not bailout after observing that loading is needed and render the code with lang "plain" + theme "none" which are always loaded

editor.update(() => {
const codeNode = $getNodeByKey(codeNodeKey);
if ($isCodeNode(codeNode)) {
codeNode.setTheme(theme);
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.

There should be some way to do this only once when the theme transitions from a not loaded to a loaded state to avoid running the transforms more times than necessary

Suggested change
codeNode.setTheme(theme);
codeNode.markDirty();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, codeNode.markDirty(); is sufficient indeed. I am not sure if your comment refers to an additional optimization that could be done to lower the number of transform applications between the time when we receive the setTheme & the time we know the theme has been loaded.

I am not sure I am fully aware of what is the overhead difference between codeNode.setTheme(theme); and codeNode.markDirty();.

@jeromew
Copy link
Copy Markdown
Contributor Author

jeromew commented Jul 1, 2025

@etrepum thanks for the comments I will look into them. For now I am blocked on the build process in scripts/build.js that seems to not like my dynamic imports. I will try to find time to look into it but am not familiar with this script and the extent to which I can change it.

i think the problem arises when the build script tries to bundle https://github.com/shikijs/shiki/blob/main/packages/shiki/src/langs-bundle-full.ts and https://github.com/shikijs/shiki/blob/main/packages/shiki/src/themes.ts which have all the dynamic imports for all shiki languages

@etrepum
Copy link
Copy Markdown
Collaborator

etrepum commented Jul 1, 2025

Just do it without the async imports for now and that can be handled separately.

etrepum
etrepum previously approved these changes Jul 11, 2025
Copy link
Copy Markdown
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

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

I think this is in a good state! I do like the idea of a line-based approach for shiki, which will certainly help with other UI affordances like making the gutters correct, but I think that can be handled as a follow-up. We might want to do some additional doc changes to note that this package is experimental and we expect to change the internals for that purpose.

Any thoughts on adding this @lexical/code-shiki engine? /cc @ivailop7 @zurfyx

The big highlights for me are:

  • Shiki is a maintained dependency
  • It leverages a standard TextMate grammar format for its syntax (same as vscode)
  • Lazy loading of language and theme files without too many hacks and without risk of breaking prism

Some follow-up items to consider:

  • Extracting prism to its own optional package (breaking change)
  • Build time preloading of syntax and theme modules
  • Restructuring shiki highlighted code blocks to be line-oriented (and having a gutter implementation for it)

@etrepum
Copy link
Copy Markdown
Collaborator

etrepum commented Jul 11, 2025

One thing that would be helpful while this gets some feedback from other maintainers would be to reformat the PR title/description to match the pull request template

It would also be good to note the (minor) breaking change prominently, typically with something like this:

## Breaking Changes

The `data-highlight-language` DOM attribute is now applied to CodeNode after highlighting, rather than immediately. The `data-language` attribute behavior is unchanged.

@jeromew jeromew changed the title [lexical-code][lexical-playground] Refactor lexical-code to add Shiki support [lexical-code][lexical-code-shiki][lexical-markdown][lexical-playground][lexical-devtools] Feature: Experimental Shiki support for code highlighting Jul 11, 2025
Copy link
Copy Markdown
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

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

Made some minor changes, primarily to preserve constructor signature compatibility with the existing CodeNode

@fantactuka
Copy link
Copy Markdown
Collaborator

fantactuka commented Jul 15, 2025

Some follow-up items to consider

One other thing we've been missing is just pure <code> element that has no dependencies on highlighting. E.g., with code as part of rich text package, and highlighting being a separate package that handles parsing/tokenization using 3p libraries.

@etrepum
Copy link
Copy Markdown
Collaborator

etrepum commented Jul 15, 2025

One other thing we've been missing is just pure <code> element that has no dependencies on highlighting

This PR is a big step in that direction, it decoupled CodeNode from Prism and any highlighting dependencies (which is why the timing of setting the highlight language attribute changed). The remaining step would primarily be to break compatibility by moving the Prism code to its own package.

@etrepum
Copy link
Copy Markdown
Collaborator

etrepum commented Jul 15, 2025

You can test out the playground with shiki enabled here:
https://lexical-playground-git-fork-jeromew-code-shiki-fbopensource.vercel.app/?isCodeShiki=true

You can see it without any highlighting plugin enabled here:
https://lexical-playground-git-fork-jeromew-code-shiki-fbopensource.vercel.app/?isCodeHighlighted=false

@etrepum etrepum added this pull request to the merge queue Jul 18, 2025
Merged via the queue into facebook:main with commit 695fd3d Jul 18, 2025
40 checks passed
@etrepum etrepum mentioned this pull request Aug 7, 2025
fantactuka pushed a commit that referenced this pull request Aug 11, 2025
…nd][lexical-devtools] Feature: Experimental Shiki support for code highlighting (#7662)

Co-authored-by: Bob Ippolito <bob@redivi.com>
@vadimkantorov
Copy link
Copy Markdown

vadimkantorov commented Aug 17, 2025

This PR is a big step in that direction, it decoupled CodeNode from Prism and any highlighting dependencies

Awesome :)

Like so, lexical-markdown can import triple-backticks and still render it maybe even without dependencies on Prism/Shiki (if highlighting is not needed, just some display of block of code) or even on lexical-code

@etrepum
Copy link
Copy Markdown
Collaborator

etrepum commented Aug 17, 2025

CodeNode will remain in lexical-code, eventually prism support will be moved to an optional package.

GermanJablo added a commit to payloadcms/payload that referenced this pull request Sep 3, 2025
Fixes #13386

Below I write a clarification to copy and paste into the release note,
based on our latest upgrade of Lexical [in
v3.29.0](https://github.com/payloadcms/payload/releases/tag/v3.29.0).

## Important
This release upgrades the lexical dependency from 0.28.0 to 0.34.0.

If you installed lexical manually, update it to 0.34.0. Installing
lexical manually is not recommended, as it may break between updates,
and our re-exported versions should be used. See the [yellow banner
box](https://payloadcms.com/docs/rich-text/custom-features) for details.

If you still encounter richtext-lexical errors, do the following, in
this order:

- Delete node_modules
- Delete your lockfile (e.g. pnpm-lock.json)
- Reinstall your dependencies (e.g. pnpm install)

### Lexical Breaking Changes

The following Lexical releases describe breaking changes. We recommend
reading them if you're using Lexical APIs directly
(`@payloadcms/richtext-lexical/lexical/*`).

- [v.0.33.0](https://github.com/facebook/lexical/releases/tag/v0.33.0)
- [v.0.30.0](https://github.com/facebook/lexical/releases/tag/v0.30.0)
- [v.0.29.0](https://github.com/facebook/lexical/releases/tag/v0.29.0)

___

TODO:
- [x] facebook/lexical#7719
- [x] facebook/lexical#7362
- [x] facebook/lexical#7707
- [x] facebook/lexical#7388
- [x] facebook/lexical#7357
- [x] facebook/lexical#7352
- [x] facebook/lexical#7472
- [x] facebook/lexical#7556
- [x] facebook/lexical#7417
- [x] facebook/lexical#1036
- [x] facebook/lexical#7509
- [x] facebook/lexical#7693
- [x] facebook/lexical#7408
- [x] facebook/lexical#7450
- [x] facebook/lexical#7415
- [x] facebook/lexical#7368
- [x] facebook/lexical#7372
- [x] facebook/lexical#7572
- [x] facebook/lexical#7558
- [x] facebook/lexical#7613
- [x] facebook/lexical#7405
- [x] facebook/lexical#7420
- [x] facebook/lexical#7662

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211202581885926
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.

5 participants