Files
openchamber/packages/ui/src/lib/codemirror/languageByExtension.ts
T
Shyamalan KannanandShyamalan Kannan ecb22e19c3 perf: lazy-load heavy dependencies (MarkdownRenderer + CodeMirror languages) (#997)
* perf: drastically improve cold-start, bundle size, and streaming performance

Cold-start optimizations:
- main.tsx: Remove blocking await on prefs I/O — render immediately with
  defaults, hydrate persisted settings asynchronously. Cuts 50-200ms from
  time-to-first-paint.
- bootstrap.ts: Split directory bootstrap into 3 phases:
  * Phase 1 (blocking): path, config, provider, session status — minimum
    data needed to render UI. Mark status complete after this phase.
    path.get and session.status must both succeed; they have no fallback.
  * Phase 2 (deferred): agents, commands, mcp, lsp, vcs, questions,
    permissions — fetched after first paint without blocking.
  * Phase 3 (lazy): session messages — loaded without blocking init.
- App.tsx: Keep identical provider tree before/after init to prevent
  full subtree remount when isInitialized flips. FireworksProvider and
  VoiceProvider are lightweight shells; overlays deferred until init.

Bundle-size optimizations:
- App.tsx + MainLayout.tsx + VSCodeLayout.tsx: Code-split heavy views
  (SettingsView, GitView, DiffView, TerminalView, FilesView, PlanView,
  OnboardingScreen, SettingsWindow, MultiRunWindow) with React.lazy.
  Views load on demand when user switches panels.
- vite.config.ts: Lower chunkSizeWarningLimit from 1200KB to 500KB.

Streaming render optimizations:
- streaming.ts: Throttle streaming store writes ~60Hz → ~1Hz. Busy-session
  only scan (Set, O(1)).
- MessageList.tsx: Lower virtualization threshold 40 → 15.
- ChatMessage.tsx: React.memo with areRenderRelevantMessagesEqual.
- MarkdownRenderer.tsx: React.memo with explicit prop comparators.

* fix: address Greptile review feedback on bootstrap and provider tree

- bootstrap.ts: Tighten Phase 1 error guard. path.get and session.status
  must both succeed; they have no global fallback.
- bootstrap.ts: Replace dead .catch() on Promise.allSettled() with .then()
  that inspects individual results for errors.
- App.tsx: Keep identical provider tree before/after init to prevent full
  subtree remount when isInitialized flips.

* perf: lazy-load heavy dependencies (MarkdownRenderer + CodeMirror languages)

MarkdownRenderer dynamic import:
- Move heavy implementation (marked, react-markdown, beautiful-mermaid,
  react-syntax-highlighter, ~1500 lines) to MarkdownRendererImpl.tsx
- Replace MarkdownRenderer.tsx with thin lazy wrapper using React.lazy
- All 11 existing imports work unchanged — no consumer code modified
- Full markdown stack loads on first render of markdown content

CodeMirror language lazy loading:
- languageByExtension.ts: remove static imports for 10+ less-common
  language packages (@codemirror/lang-go, lang-rust, lang-sql, etc.)
- Keep only 6 most common languages static: javascript, json, css, html,
  markdown, python, shell
- Less common languages return null from languageByExtension, causing
  callers to fall back to loadLanguageByExtension which dynamically
  loads from @codemirror/language-data
- Reduces initial bundle by ~200KB+ of language parsers

---------

Co-authored-by: Shyamalan Kannan <yabuku@Shyamalans-MacBook-Pro.local>
2026-04-23 12:31:42 +03:00

176 lines
5.1 KiB
TypeScript

import type { Extension } from '@codemirror/state';
// Static imports for the most common languages only.
// Less common languages are loaded dynamically via loadLanguageByExtension
// to keep the initial bundle lean.
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { css } from '@codemirror/lang-css';
import { html } from '@codemirror/lang-html';
import { markdown } from '@codemirror/lang-markdown';
import { languages } from '@codemirror/language-data';
import { python } from '@codemirror/lang-python';
import { Language, LanguageDescription, StreamLanguage, HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
import { shell } from '@codemirror/legacy-modes/mode/shell';
const shellLanguage = StreamLanguage.define(shell);
function codeBlockLanguageResolver(info: string): Language | LanguageDescription | null {
const normalized = info.trim().toLowerCase();
switch (normalized) {
case 'bash':
case 'sh':
case 'zsh':
case 'shell':
case 'shellsession':
case 'console':
return shellLanguage;
case 'json':
case 'jsonc':
case 'json5':
return json().language;
case 'js':
case 'javascript':
return javascript().language;
case 'jsx':
return javascript({ jsx: true }).language;
case 'ts':
case 'typescript':
return javascript({ typescript: true }).language;
case 'tsx':
return javascript({ typescript: true, jsx: true }).language;
case 'html':
return html().language;
case 'css':
return css().language;
case 'py':
case 'python':
return python().language;
case 'heex':
case 'eex':
case 'leex':
return html().language;
default:
return LanguageDescription.matchLanguageName(languages, normalized, true);
}
}
const normalizeFileName = (filePath: string) => filePath.split('/').pop()?.toLowerCase() ?? '';
const matchLanguageDescriptionForFile = (filePath: string): LanguageDescription | null => {
const filename = normalizeFileName(filePath);
if (!filename) {
return null;
}
return LanguageDescription.matchFilename(languages, filename);
};
const markdownHighlight = () => syntaxHighlighting(HighlightStyle.define([
{ tag: [t.heading1, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6], fontWeight: '600' },
{ tag: t.strong, fontWeight: '600' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: [t.link, t.url], color: 'var(--markdown-link, currentColor)', textDecoration: 'underline' },
{ tag: t.monospace, color: 'var(--markdown-inline-code, currentColor)', backgroundColor: 'var(--markdown-inline-code-bg, transparent)' },
{ tag: t.quote, color: 'var(--markdown-blockquote, currentColor)', fontStyle: 'italic' },
{ tag: t.list, color: 'color-mix(in srgb, var(--muted-foreground) 40%, var(--foreground) 60%)' },
{ tag: t.heading, color: 'var(--markdown-heading1, currentColor)' },
]));
export function languageByExtension(filePath: string): Extension | null {
const normalized = filePath.toLowerCase();
const filename = normalizeFileName(normalized);
// Special filenames
switch (filename) {
case 'makefile':
case 'gnumakefile':
// No dedicated mode; shell is a decent fallback for Make-ish files.
return shellLanguage;
}
const idx = normalized.lastIndexOf('.');
const ext = idx >= 0 ? normalized.slice(idx + 1) : '';
switch (ext) {
// JavaScript/TypeScript (most common — keep static)
case 'ts':
case 'tsx':
case 'mts':
case 'cts':
return javascript({ typescript: true, jsx: ext === 'tsx' });
case 'js':
case 'jsx':
case 'mjs':
case 'cjs':
return javascript({ typescript: false, jsx: ext === 'jsx' });
// Web (keep static)
case 'json':
case 'jsonc':
case 'json5':
case 'jsonl':
case 'ndjson':
case 'geojson':
return json();
case 'css':
case 'scss':
case 'sass':
case 'less':
return css();
case 'html':
case 'htm':
return html();
case 'md':
case 'mdx':
case 'markdown':
case 'mdown':
case 'mkd':
return [
markdown({
codeLanguages: codeBlockLanguageResolver,
}),
markdownHighlight(),
];
// Shell (keep static)
case 'sh':
case 'bash':
case 'zsh':
case 'fish':
case 'env':
return shellLanguage;
// Python (very common — keep static)
case 'py':
case 'pyw':
case 'pyi':
return python();
// Less common languages: return null so callers fall back to
// loadLanguageByExtension which dynamically imports from @codemirror/language-data.
default:
return null;
}
}
export async function loadLanguageByExtension(filePath: string): Promise<Extension | null> {
const description = matchLanguageDescriptionForFile(filePath);
if (!description) {
return null;
}
if (description.support) {
return description.support;
}
try {
return await description.load();
} catch {
return null;
}
}