Repository navigation
perf(markdown): update code highlighting incrementally - #17147
Conversation
Greptile SummaryThe PR replaces full-document syntax-highlighting recomputation with incremental decoration updates while preserving the existing code-block integration.
Confidence Score: 5/5The PR appears safe to merge with no blocking failure remaining. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| src/renderer/src/components/editor/rich-markdown-lowlight.ts | Introduces incremental syntax-highlight decoration mapping, targeted invalidation, and conservative full-document fallback behavior. |
| src/renderer/src/components/editor/rich-markdown-lowlight.test.ts | Adds broad parity and performance-focused coverage against the stock Tiptap lowlight extension. |
| src/renderer/src/components/editor/rich-markdown-extensions.ts | Replaces the stock lowlight extension with the incremental implementation while retaining the custom node view. |
| package.json | Declares the direct Tiptap code-block dependency required by the new extension implementation. |
| pnpm-lock.yaml | Records the new direct dependency without changing its resolved package version. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
T[Editor transaction] --> D{Document changed?}
D -- No --> K[Keep existing decorations]
D -- Yes --> R[Compute changed ranges]
R --> L{Ranges localized?}
L -- No --> F[Re-highlight all code blocks]
L -- Yes --> M[Map existing decorations]
M --> S[Remove decorations from affected old blocks]
S --> H[Highlight affected new blocks]
H --> O[Return updated decoration set]
F --> O
Reviews (2): Last reviewed commit: "perf(markdown): update code highlighting..." | Re-trigger Greptile
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe PR adds Merge Risk: 🔵 Low · up to Unsupported or custom language labels may cause code highlighting to throw instead of falling back gracefully, disrupting Markdown editing or rendering for affected blocks. The PR is otherwise mergeable, but this compatibility fix should be applied or explicitly accepted by the owner. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 3 files. (1 skipped: 1 unsupported.) Full details: Description checkExplanation The description is mostly complete and directly explains the change, motivation, performance results, testing, regression coverage, and merge risk. It includes a linked issue reference and before/after measurements. Some template headings and checklist items are not reproduced exactly, but the required decision-making information is present.
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 070c90d8-b1d7-4d3f-8fcf-5ea67801ce7f
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (4)
package.jsonsrc/renderer/src/components/editor/rich-markdown-extensions.tssrc/renderer/src/components/editor/rich-markdown-lowlight.test.tssrc/renderer/src/components/editor/rich-markdown-lowlight.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.
| ): Decoration[] { | ||
| const language = (block.node.attrs.language as string | null | undefined) || defaultLanguage | ||
| const highlighted = | ||
| language && (languages.has(language) || lowlight.registered(language)) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect all configurations of the replacement extension and their Lowlight values.
rg -n -C 4 --glob '*.{ts,tsx}' \
'RichMarkdownCodeBlockLowlight|lowlight\s*:' srcRepository: stablyai/orca
Length of output: 10894
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- repository conventions and learnings ---'
find /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4 -type f -name '*.md' -maxdepth 3 -print \
-exec sh -c 'echo "--- $1"; head -80 "$1"' sh {} \;
printf '%s\n' '--- reviewed implementation ---'
cat -n src/renderer/src/components/editor/rich-markdown-lowlight.ts | sed -n '1,230p'
printf '%s\n' '--- extension configuration ---'
cat -n src/renderer/src/components/editor/rich-markdown-extensions.ts | sed -n '1,100p'
printf '%s\n' '--- dependency declarations ---'
rg -n -C 3 '"(lowlight|`@tiptap/extension-code-block-lowlight`)"' \
package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true
printf '%s\n' '--- relevant tests and option-shape references ---'
cat -n src/renderer/src/components/editor/rich-markdown-lowlight.test.ts | sed -n '1,100p'
rg -n -C 4 'registered|listLanguages|highlightAuto|CodeBlockLowlightOptions|CodeBlockLowlight' \
src package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || trueRepository: stablyai/orca
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- knowledge file names ---'
find /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4 -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- reviewed implementation ---'
cat -n src/renderer/src/components/editor/rich-markdown-lowlight.ts | sed -n '1,225p'
printf '%s\n' '--- configured instance ---'
cat -n src/renderer/src/components/editor/rich-markdown-extensions.ts | sed -n '34,85p'
printf '%s\n' '--- package metadata ---'
rg -n -C 3 '"(lowlight|`@tiptap/extension-code-block-lowlight`)"' \
package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true
printf '%s\n' '--- lowlight-related source references in editor scope ---'
rg -n -C 3 'registered|listLanguages|highlightAuto|CodeBlockLowlightOptions|CodeBlockLowlight' \
src/renderer/src/components/editorRepository: stablyai/orca
Length of output: 40392
🌐 Web query:
@tiptap/extension-code-block-lowlight v3.22.5 source CodeBlockLowlightOptions registered lowlight
💡 Result:
To use syntax highlighting in the @tiptap/extension-code-block-lowlight extension (version 3.22.5), you must provide a configured lowlight instance to the extension's options [1][2]. The extension does not ship with built-in languages, so you must initialize lowlight and register the languages you wish to support [3][4]. ### Implementation Steps 1. Install the required dependencies: npm install lowlight @tiptap/extension-code-block-lowlight 2. Import createLowlight and the desired languages (or the common bundle) from the lowlight package [3][4]. 3. Create and configure the lowlight instance [3][4]. 4. Pass this instance to the CodeBlockLowlight extension via its configuration [1][2]. ### Example Configuration import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight'; import { createLowlight, common } from 'lowlight'; // Initialize lowlight with common languages const lowlight = createLowlight(common); const editor = new Editor({ extensions: [ // Ensure the default code block from StarterKit is disabled to avoid conflicts CodeBlockLowlight.configure({ lowlight, }), ], }); ### Important Notes - Language Registration: If you prefer not to load all common languages, you can create an empty lowlight instance and manually register only the languages you need using lowlight.register({ languageName }) [3]. - Configuration Options: Beyond the lowlight instance, you can configure other options such as defaultLanguage, enableTabIndentation, tabSize, and HTMLAttributes [1][5]. - Conflict Warning: If using StarterKit, ensure the default codeBlock extension is disabled (codeBlock: false) to prevent duplicate extension name conflicts [4].
Citations:
- 1: https://tiptap.dev/docs/editor/extensions/nodes/code-block-lowlight
- 2: https://github.com/ueberdosis/tiptap-docs/blob/main/src/content/editor/extensions/nodes/code-block-lowlight.mdx
- 3: GitHub issue 4874 in ueberdosis/tiptap (link omitted to avoid creating a cross-reference)
- 4: https://techqa.club/v/q/how-do-you-highlight-codeblock-with-tiptap-77573628
- 5: https://tessl.io/registry/tessl/npm-tiptap--extension-code-block-lowlight/3.4.0
🌐 Web query:
lowlight v3.3.0 createLowlight registered API source
💡 Result:
In lowlight v3.3.0, the createLowlight function is a named export used to create a new, isolated highlighter instance [1][2]. It does not have a "registered API source" in the sense of a specific configuration file; rather, it is designed to be populated with language grammars dynamically [3]. To use createLowlight, you import it from the lowlight package [1][4]. Because createLowlight() starts with no syntaxes included by default, you must pass the desired grammars to it [1][2]. You can either pass the pre-packaged common or all language bundles, or register custom grammars [1][3]. Example usage: import {common, createLowlight} from 'lowlight' // Create an instance pre-populated with common languages const lowlight = createLowlight(common) // Alternatively, create an empty instance and register specific grammars manually const lowlightCustom = createLowlight import javascript from 'highlight.js/lib/languages/javascript' lowlightCustom.register({javascript}) The library provides several methods to manage these registrations: - lowlight.register(grammars): Registers one or more languages [3][5]. - lowlight.registered(aliasOrlanguage): Checks if a specific language or alias is already registered [3][5]. - lowlight.registerAlias(aliases): Registers aliases for existing languages [3][5].
Citations:
- 1: https://npmx.dev/package/lowlight/v/3.3.0
- 2: https://github.com/wooorm/lowlight/
- 3: https://github.com/wooorm/lowlight/blob/main/readme.md
- 4: https://github.com/wooorm/lowlight?tab=readme-ov-file
- 5: https://npmx.dev/package/lowlight/v/3.1.0
🌐 Web query:
site:github.com/ueberdosis/tiptap "lowlight.registered" "CodeBlockLowlight"
💡 Result:
In the context of the Tiptap editor extension @tiptap/extension-code-block-lowlight, the usage of lowlight (the library) has evolved to accommodate changes in lowlight version 3.x [1]. Starting with lowlight v3.1.0, you must explicitly create a lowlight instance and register the specific programming languages you intend to use [1][2]. This is done using the createLowlight factory function [1][2]. Here is the standard implementation pattern: 1. Import the necessary functions and languages [1][2]. 2. Create a lowlight instance using createLowlight() (optionally passing common or all to pre-load a set of languages) [1][2]. 3. Register individual languages on that instance using the .register() method [1][2]. 4. Configure the CodeBlockLowlight extension by passing your lowlight instance [1][2]. Example implementation: import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight'; import { createLowlight } from 'lowlight'; import javascript from 'highlight.js/lib/languages/javascript'; // 1. Create instance const lowlight = createLowlight; // 2. Register languages lowlight.register('js', javascript); // 3. Configure Tiptap extension const editor = new Editor({ extensions: [ CodeBlockLowlight.configure({ lowlight, defaultLanguage: 'javascript', // Optional: sets default if no language is specified }), ], }); In older versions (prior to v3), lowlight often exported a singleton object directly, which was typically registered via lowlight.registerLanguage('lang', lang). The move to createLowlight was introduced to better support modularity and modern ESM environments [1][3]. Common issues often arise from: - Not calling createLowlight and attempting to use an undefined or older-style export [1][4]. - Using mismatched versions of lowlight and highlight.js [1][5]. - Conflicts with the default Tiptap CodeBlock extension; it is recommended to disable the standard codeBlock in StarterKit if you are using CodeBlockLowlight to avoid duplicate extension name warnings [6][7].
Citations:
- 1: GitHub issue 4874 in ueberdosis/tiptap (link omitted to avoid creating a cross-reference)
- 2: ueberdosis/tiptap@v2.5.8...v2.5.9
- 3: GitHub issue 2116 in ueberdosis/tiptap (link omitted to avoid creating a cross-reference)
- 4: GitHub issue 3234 in ueberdosis/tiptap (link omitted to avoid creating a cross-reference)
- 5: GitHub discussion 2191 in ueberdosis/tiptap (link omitted to avoid creating a cross-reference)
- 6: GitHub discussion 3030 in ueberdosis/tiptap (link omitted to avoid creating a cross-reference)
- 7: GitHub discussion 2121 in ueberdosis/tiptap (link omitted to avoid creating a cross-reference)
🌐 Web query:
"CodeBlockLowlightOptions" "registered(language)" tiptap
💡 Result:
The CodeBlockLowlight extension in Tiptap does not have a "registered" method for languages directly within the extension itself [1][2]. Instead, syntax highlighting is powered by the external lowlight library, and languages must be registered on the lowlight instance that you pass to the extension [1][3]. To add support for specific programming languages, you should follow these steps: 1. Import your desired languages from highlight.js (or similar sources compatible with lowlight) [4][3]. 2. Register these languages on the lowlight instance using lowlight.register() or lowlight.registerLanguage() [4][3]. 3. Pass the configured lowlight instance into the CodeBlockLowlight extension via its configure method [1][3]. Example: javascript import { lowlight } from 'lowlight/lib/core'; import javascript from 'highlight.js/lib/languages/javascript'; import typescript from 'highlight.js/lib/languages/typescript'; // Register languages on the lowlight instance lowlight.registerLanguage('javascript', javascript); lowlight.registerLanguage('typescript', typescript); // Configure the extension with the lowlight instance const CodeBlock = CodeBlockLowlight.configure({ lowlight, }); This approach keeps the Tiptap extension lightweight by allowing your application to control exactly which version of lowlight is used and which specific language packages are bundled [1]. Options like defaultLanguage can also be set within the configure method to control the behavior of the code blocks [3].
Citations:
- 1: https://tiptap.dev/docs/editor/extensions/nodes/code-block-lowlight
- 2: https://www.npmjs.com/package/@tiptap/extension-code-block-lowlight
- 3: https://tessl.io/registry/tessl/npm-tiptap--extension-code-block-lowlight/3.4.0
- 4: GitHub issue 3234 in ueberdosis/tiptap (link omitted to avoid creating a cross-reference)
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Tiptap v3.22.5 implementation ---'
curl -fsSL https://raw.githubusercontent.com/ueberdosis/tiptap/v3.22.5/packages/extension-code-block-lowlight/src/code-block-lowlight.ts \
| nl -ba | sed -n '1,220p'
printf '%s\n' '--- Tiptap v3.22.5 package declarations ---'
curl -fsSL https://raw.githubusercontent.com/ueberdosis/tiptap/v3.22.5/packages/extension-code-block-lowlight/src/code-block-lowlight.ts \
| rg -n -C 5 'CodeBlockLowlightOptions|lowlight|registered|listLanguages|highlightAuto'
printf '%s\n' '--- lowlight v3.3.0 implementation/type declarations ---'
for path in lib/index.js types/index.d.ts; do
echo "--- $path"
curl -fsSL "https://raw.githubusercontent.com/wooorm/lowlight/3.3.0/$path" \
| rg -n -C 4 'registered|createLowlight|listLanguages|highlightAuto' || true
doneRepository: stablyai/orca
Length of output: 255
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Tiptap v3.22.5 implementation ---'
curl -fsSL https://raw.githubusercontent.com/ueberdosis/tiptap/v3.22.5/packages/extension-code-block-lowlight/src/code-block-lowlight.ts \
| cat -n | sed -n '1,220p'
printf '%s\n' '--- lowlight v3.3.0 declarations and implementation ---'
for path in lib/index.js types/index.d.ts; do
echo "--- $path"
curl -fsSL "https://raw.githubusercontent.com/wooorm/lowlight/3.3.0/$path" \
| grep -n -C 4 -E 'registered|createLowlight|listLanguages|highlightAuto' || true
doneRepository: stablyai/orca
Length of output: 6806
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Tiptap v3.22.5 LowlightPlugin ---'
curl -fsSL https://raw.githubusercontent.com/ueberdosis/tiptap/v3.22.5/packages/extension-code-block-lowlight/src/lowlight-plugin.ts \
| cat -n | sed -n '1,240p'Repository: stablyai/orca
Length of output: 5942
Keep registered optional.
RichMarkdownCodeBlockLowlight accepts the CodeBlockLowlightOptions.lowlight value, but its unknown-language branch calls lowlight.registered(language) unconditionally. Tiptap only requires highlight, highlightAuto, and listLanguages, so this can throw before highlightAuto() runs. Use lowlight.registered?.(language).
634b0bc to
82f2ea4
Compare
Summary
Performance
On a 305 KB Markdown fixture with 533 code blocks, 15 real Electron keyboard samples measured keydown to the next animation frame:
A deterministic test also verifies that a local code edit invokes highlighting for 1 block instead of all 533.
Validation
Follow-up to #17008.
ELI5
Typing in one code block used to make the editor recolor every code block in the document. The editor now carries unchanged colors forward and recolors only blocks touched by the edit. If it cannot prove which block changed, it safely falls back to recoloring everything.
Before / After measurement
On the same 305 KB, 533-code-block fixture in Electron (15 keyboard samples), keydown-to-next-frame median fell from 79.5 ms to 22.7 ms (-71%) and p95 fell from 114.5 ms to 39.7 ms (-65%). The deterministic call-count test changes one local edit from 533 highlight calls to 1.
Regression Proof
No-user-regression signoff
No highlighting mode, language option, editor command, paste behavior, or rendered syntax is removed. Unchanged decorations are mapped through the editor transaction, touched blocks use the same highlighter, and ambiguous changes take the existing full-rebuild path. There is no persistence, IPC, remote-wire, SSH, filesystem, or platform-specific behavior change.
Merge risk
Medium-low. This changes transaction-range bookkeeping in a latency-sensitive editor extension. The main risk is a missed affected range leaving stale decoration positions; boundary, structural-edit, undo/redo, and full-replacement parity cases exercise those paths, while the conservative full-rebuild fallback handles unlocalizable edits. The PR is isolated and independently revertible.