Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 18 additions & 72 deletions bun.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"@ai-sdk/openai": "^2.0.52",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@types/react-syntax-highlighter": "^15.5.13",
"ai": "^5.0.72",
"ai-tokenizer": "^1.0.3",
"chalk": "^5.6.2",
Expand All @@ -46,6 +45,7 @@
"diff": "^8.0.2",
"disposablestack": "^1.1.7",
"electron-updater": "^6.6.2",
"escape-html": "^1.0.3",
"jsonc-parser": "^3.3.1",
"lru-cache": "^11.2.2",
"markdown-it": "^14.1.0",
Expand All @@ -58,7 +58,6 @@
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.6.6",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
Expand All @@ -85,6 +84,7 @@
"@testing-library/react": "^16.3.0",
"@types/bun": "^1.2.23",
"@types/diff": "^8.0.0",
"@types/escape-html": "^1.0.4",
"@types/jest": "^30.0.0",
"@types/katex": "^0.16.7",
"@types/markdown-it": "^14.1.2",
Expand Down
99 changes: 0 additions & 99 deletions scripts/generate_prism_css.ts

This file was deleted.

21 changes: 20 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,31 @@ const globalStyles = css`
}

/* Search term highlighting - global for consistent styling across components */
mark.search-highlight {
/* Applied to <mark> for plain text and <span> for Shiki-highlighted code */
mark.search-highlight,
span.search-highlight {
background: rgba(255, 215, 0, 0.3);
color: inherit;
padding: 0;
border-radius: 2px;
}

/* Override Shiki theme background to use our global color */
.shiki,
.shiki pre {
background: var(--color-code-bg) !important;
}

/* Global styling for markdown code blocks */
pre code {
display: block;
background: var(--color-code-bg);
margin: 1em 0;
border-radius: 4px;
font-size: 12px;
padding: 12px;
overflow: auto;
}
`;

// Styled Components
Expand Down
105 changes: 73 additions & 32 deletions src/components/Messages/MarkdownComponents.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { ReactNode } from "react";
import React from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { syntaxStyleNoBackgrounds } from "@/styles/syntaxHighlighting";
import React, { useState, useEffect } from "react";
import { Mermaid } from "./Mermaid";
import {
getShikiHighlighter,
mapToShikiLang,
SHIKI_THEME,
} from "@/utils/highlighting/shikiHighlighter";

interface CodeProps {
node?: unknown;
Expand All @@ -24,6 +27,63 @@ interface SummaryProps {
children?: ReactNode;
}

interface CodeBlockProps {
code: string;
language: string;
}

/**
* CodeBlock component with async Shiki highlighting
* Reuses shared highlighter instance from diff rendering
*/
const CodeBlock: React.FC<CodeBlockProps> = ({ code, language }) => {
const [html, setHtml] = useState<string | null>(null);

useEffect(() => {
let cancelled = false;

async function highlight() {
try {
const highlighter = await getShikiHighlighter();
const shikiLang = mapToShikiLang(language);

// codeToHtml lazy-loads languages automatically
const result = highlighter.codeToHtml(code, {
lang: shikiLang,
theme: SHIKI_THEME,
});

if (!cancelled) {
setHtml(result);
}
} catch (error) {
console.warn(`Failed to highlight code block (${language}):`, error);
if (!cancelled) {
setHtml(null);
}
}
}

void highlight();

return () => {
cancelled = true;
};
}, [code, language]);

// Show loading state or fall back to plain code
if (html === null) {
return (
<pre>
<code>{code}</code>
</pre>
);
}

// Render highlighted HTML
return <div dangerouslySetInnerHTML={{ __html: html }} />;
};

// Custom components for markdown rendering
export const markdownComponents = {
// Pass through pre element - let code component handle the wrapping
Expand Down Expand Up @@ -58,48 +118,29 @@ export const markdownComponents = {
</summary>
),

// Custom code block renderer with syntax highlighting
// Custom code block renderer with async Shiki highlighting
code: ({ inline, className, children, node, ...props }: CodeProps) => {
const match = /language-(\w+)/.exec(className ?? "");
const language = match ? match[1] : "";

// Better inline detection: check for multiline content
// Extract text content
const childString =
typeof children === "string" ? children : Array.isArray(children) ? children.join("") : "";
const hasMultipleLines = childString.includes("\n");
const isInline = inline ?? !hasMultipleLines;

if (!isInline && language) {
// Extract text content from children (react-markdown passes string or array of strings)
const code =
typeof children === "string" ? children : Array.isArray(children) ? children.join("") : "";

// Handle mermaid diagrams
if (language === "mermaid") {
return <Mermaid chart={code} />;
}
// Handle mermaid diagrams specially
if (!isInline && language === "mermaid") {
return <Mermaid chart={childString} />;
}

// Code block with language - use syntax highlighter
return (
<SyntaxHighlighter
style={syntaxStyleNoBackgrounds}
language={language}
PreTag="pre"
customStyle={{
background: "rgba(0, 0, 0, 0.3)",
margin: "1em 0",
borderRadius: "4px",
fontSize: "12px",
padding: "12px",
}}
>
{code.replace(/\n$/, "")}
</SyntaxHighlighter>
);
// Code blocks with language - use async Shiki highlighting
if (!isInline && language) {
return <CodeBlock code={childString} language={language} />;
}

// Code blocks without language (global CSS provides styling)
if (!isInline) {
// Code block without language - plain pre/code
return (
<pre>
<code className={className} {...props}>
Expand Down
5 changes: 2 additions & 3 deletions src/components/RightSidebar/CodeReview/HunkViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import type { DiffHunk } from "@/types/review";
import { SelectableDiffRenderer } from "../../shared/DiffRenderer";
import {
type SearchHighlightConfig,
highlightSearchMatches,
highlightSearchInText,
} from "@/utils/highlighting/highlightSearchTerms";
import { escapeHtml } from "@/utils/highlighting/highlightDiffChunk";
import { Tooltip, TooltipWrapper } from "../../Tooltip";
import { usePersistedState } from "@/hooks/usePersistedState";
import { getReviewExpandStateKey } from "@/constants/storage";
Expand Down Expand Up @@ -212,7 +211,7 @@ export const HunkViewer = React.memo<HunkViewerProps>(
if (!searchConfig) {
return hunk.filePath;
}
return highlightSearchMatches(escapeHtml(hunk.filePath), searchConfig);
return highlightSearchInText(hunk.filePath, searchConfig);
}, [hunk.filePath, searchConfig]);

// Persist manual expand/collapse state across remounts per workspace
Expand Down
7 changes: 4 additions & 3 deletions src/components/shared/DiffRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ export const LineContent = styled.span<{ type: DiffLineType }>`
}};

/* Ensure Shiki spans don't interfere with diff backgrounds */
span {
/* Exclude search-highlight to allow search marking to show */
span:not(.search-highlight) {
background: transparent !important;
}
`;
Expand Down Expand Up @@ -156,7 +157,7 @@ interface DiffRendererProps {

/**
* Hook to pre-process and highlight diff content in chunks
* Runs once when content/language changes
* Runs once when content/language changes (NOT search - that's applied post-process)
*/
function useHighlightedDiff(
content: string,
Expand All @@ -176,7 +177,7 @@ function useHighlightedDiff(
// Group into chunks
const diffChunks = groupDiffLines(lines, oldStart, newStart);

// Highlight each chunk
// Highlight each chunk (without search decorations - those are applied later)
const highlighted = await Promise.all(
diffChunks.map((chunk) => highlightDiffChunk(chunk, language))
);
Expand Down
Loading
Loading