Skip to content

perf(markdown): update code highlighting incrementally - #17147

Merged
nwparker merged 1 commit into
mainfrom
nwparker/perf-incremental-code-highlighting
Aug 29, 2026
Merged

nwparker merged 1 commit into
mainfrom
nwparker/perf-incremental-code-highlighting

Conversation

@nwparker

@nwparker nwparker commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​467 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​467
Prod 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​225 $\color{#cf222e}{\Huge{\mathbf{−}}}$​2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​223

Summary

  • map syntax-highlight decorations through unrelated document changes
  • recompute only code blocks touched by transaction ranges, with a full rebuild when a change cannot be localized safely
  • preserve existing code-block options, commands, node views, and paste behavior while adding stock-parity coverage

Performance

On a 305 KB Markdown fixture with 533 code blocks, 15 real Electron keyboard samples measured keydown to the next animation frame:

  • median: 79.5 ms to 22.7 ms (71% faster)
  • p95: 114.5 ms to 39.7 ms (65% faster)

A deterministic test also verifies that a local code edit invokes highlighting for 1 block instead of all 533.

Validation

  • 18 stock-parity tests covering language handling, prose and code edits, insert/delete, undo/redo, conversions, paste, split/join, moves, nested blocks, full replacement, selection-only changes, adjacent boundaries, options, and paste-handler preservation
  • 181 renderer-editor test files passed, 1,224 tests total
  • full typecheck and post-rebase web typecheck passed
  • changed-code quality gate passed with zero findings
  • production Electron build passed
  • Electron CDP check confirmed 533/533 rendered blocks highlighted; first and last blocks retained highlight spans; real typing and undo restored the document

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

  • Eighteen stock-parity tests cover language handling, prose/code edits, insert/delete, undo/redo, conversions, paste, split/join, moves, nested blocks, full replacement, selection-only changes, adjacent boundaries, options, and paste-handler preservation.
  • All 181 renderer-editor test files pass: 1,224 tests total.
  • Electron CDP confirms 533/533 blocks remain highlighted, including the first and last; real typing and undo restore the expected document.
  • Typecheck, changed-code quality, and production Electron build pass.

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.

@greptile-apps

greptile-apps Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

Greptile Summary

The PR replaces full-document syntax-highlighting recomputation with incremental decoration updates while preserving the existing code-block integration.

  • Maps unaffected highlight decorations through document transactions.
  • Re-highlights code blocks intersecting changed ranges and falls back to a complete rebuild when ranges cannot be localized.
  • Adds stock-parity and performance coverage for editing, structural transformations, history, paste behavior, options, and large documents.
  • Adds the direct Tiptap code-block dependency used by the replacement extension.

Confidence Score: 5/5

The PR appears safe to merge with no blocking failure remaining.

No blocking failure remains.

Important Files Changed

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
Loading

Reviews (2): Last reviewed commit: "perf(markdown): update code highlighting..." | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 84305d23-3934-44a2-8fad-0e8b3c87fefc

📥 Commits

Reviewing files that changed from the base of the PR and between 634b0bc and 82f2ea4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • package.json
  • src/renderer/src/components/editor/rich-markdown-extensions.ts

📝 Walkthrough

Walkthrough

The PR adds RichMarkdownCodeBlockLowlight, which uses ProseMirror decorations and Lowlight to highlight code blocks. It refreshes only affected blocks after document changes and handles language attributes, insertions, deletions, and structural edits. The rich Markdown editor now uses this extension. Tests compare its document and decoration output with Tiptap’s standard extension across editing scenarios and a 533-block stress case.

Merge Risk: 🔵 Low · up to 634b0

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)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning 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… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary change: incremental Markdown code highlighting for improved performance.
Description check ✅ Passed 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/afte…
Full details: Docstring Coverage

Explanation

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 check

Explanation

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.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between fb6c280 and 634b0bc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • package.json
  • src/renderer/src/components/editor/rich-markdown-extensions.ts
  • src/renderer/src/components/editor/rich-markdown-lowlight.test.ts
  • src/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))

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.

🩺 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*:' src

Repository: 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 || true

Repository: 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/editor

Repository: 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:


🌐 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:


🌐 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:


🏁 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
done

Repository: 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
done

Repository: 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).

@nwparker
nwparker force-pushed the nwparker/perf-incremental-code-highlighting branch from 634b0bc to 82f2ea4 Compare August 29, 2026 20:52
@nwparker
nwparker merged commit 0bf5361 into main Aug 29, 2026
4 checks passed
@nwparker nwparker mentioned this pull request Aug 29, 2026
9 tasks done
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant