Replace react-syntax-highlighter with rehype-highlight to reduce memory footprint when scrolling code blocks

This commit is contained in:
Saoud Rizwan
2024-09-06 21:06:36 -04:00
parent 428d3c39b5
commit ea14481771
26 changed files with 3920 additions and 2051 deletions

View File

@@ -1,17 +1,15 @@
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import React, { memo, useMemo } from "react"
import ReactMarkdown from "react-markdown"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { ClaudeMessage, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences"
import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme"
import CodeAccordian from "./CodeAccordian"
import CodeBlock from "./CodeBlock"
import Terminal from "./Terminal"
import Thumbnails from "./Thumbnails"
interface ChatRowProps {
message: ClaudeMessage
syntaxHighlighterStyle: SyntaxHighlighterStyle
isExpanded: boolean
onToggleExpand: () => void
lastModifiedMessage?: ClaudeMessage
@@ -35,7 +33,6 @@ export default ChatRow
const ChatRowContent = ({
message,
syntaxHighlighterStyle,
isExpanded,
onToggleExpand,
lastModifiedMessage,
@@ -165,10 +162,9 @@ const ChatRowContent = ({
{toolIcon("edit")}
<span style={{ fontWeight: "bold" }}>Claude wants to edit this file:</span>
</div>
<CodeBlock
<CodeAccordian
diff={tool.diff!}
path={tool.path!}
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={isExpanded}
onToggleExpand={onToggleExpand}
/>
@@ -181,10 +177,9 @@ const ChatRowContent = ({
{toolIcon("new-file")}
<span style={{ fontWeight: "bold" }}>Claude wants to create a new file:</span>
</div>
<CodeBlock
<CodeAccordian
code={tool.content!}
path={tool.path!}
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={isExpanded}
onToggleExpand={onToggleExpand}
/>
@@ -199,10 +194,9 @@ const ChatRowContent = ({
{message.type === "ask" ? "Claude wants to read this file:" : "Claude read this file:"}
</span>
</div>
<CodeBlock
<CodeAccordian
code={tool.content!}
path={tool.path!}
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={isExpanded}
onToggleExpand={onToggleExpand}
/>
@@ -219,11 +213,10 @@ const ChatRowContent = ({
: "Claude viewed the top level files in this directory:"}
</span>
</div>
<CodeBlock
<CodeAccordian
code={tool.content!}
path={tool.path!}
language="shell-session"
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={isExpanded}
onToggleExpand={onToggleExpand}
/>
@@ -240,11 +233,10 @@ const ChatRowContent = ({
: "Claude recursively viewed all files in this directory:"}
</span>
</div>
<CodeBlock
<CodeAccordian
code={tool.content!}
path={tool.path!}
language="shell-session"
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={isExpanded}
onToggleExpand={onToggleExpand}
/>
@@ -261,10 +253,9 @@ const ChatRowContent = ({
: "Claude viewed source code definition names used in this directory:"}
</span>
</div>
<CodeBlock
<CodeAccordian
code={tool.content!}
path={tool.path!}
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={isExpanded}
onToggleExpand={onToggleExpand}
/>
@@ -287,11 +278,10 @@ const ChatRowContent = ({
)}
</span>
</div>
<CodeBlock
<CodeAccordian
code={tool.content!}
path={tool.path! + (tool.filePattern ? `/(${tool.filePattern})` : "")}
language="plaintext"
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={isExpanded}
onToggleExpand={onToggleExpand}
/>
@@ -365,10 +355,9 @@ const ChatRowContent = ({
{isExpanded && (
<div style={{ marginTop: "10px" }}>
<CodeBlock
<CodeAccordian
code={JSON.stringify(JSON.parse(message.text || "{}").request, null, 2)}
language="json"
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={true}
onToggleExpand={onToggleExpand}
/>
@@ -381,7 +370,7 @@ const ChatRowContent = ({
case "text":
return (
<div>
<Markdown syntaxHighlighterStyle={syntaxHighlighterStyle} markdown={message.text} />
<Markdown markdown={message.text} />
</div>
)
case "user_feedback":
@@ -421,10 +410,9 @@ const ChatRowContent = ({
}}>
The user made the following changes:
</span>
<CodeBlock
<CodeAccordian
diff={tool.diff!}
path={tool.path!}
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={isExpanded}
onToggleExpand={onToggleExpand}
/>
@@ -450,7 +438,7 @@ const ChatRowContent = ({
{title}
</div>
<div style={{ color: "var(--vscode-charts-green)" }}>
<Markdown syntaxHighlighterStyle={syntaxHighlighterStyle} markdown={message.text} />
<Markdown markdown={message.text} />
</div>
</>
)
@@ -465,7 +453,7 @@ const ChatRowContent = ({
</div>
)}
<div>
<Markdown syntaxHighlighterStyle={syntaxHighlighterStyle} markdown={message.text} />
<Markdown markdown={message.text} />
</div>
</>
)
@@ -517,7 +505,7 @@ const ChatRowContent = ({
{title}
</div>
<div style={{ color: "var(--vscode-charts-green)" }}>
<Markdown syntaxHighlighterStyle={syntaxHighlighterStyle} markdown={message.text} />
<Markdown markdown={message.text} />
</div>
</div>
)
@@ -534,7 +522,7 @@ const ChatRowContent = ({
</div>
)}
<div>
<Markdown syntaxHighlighterStyle={syntaxHighlighterStyle} markdown={message.text} />
<Markdown markdown={message.text} />
</div>
</>
)
@@ -559,114 +547,94 @@ const ProgressIndicator = () => (
</div>
)
const Markdown = memo(
({ syntaxHighlighterStyle, markdown }: { syntaxHighlighterStyle: SyntaxHighlighterStyle; markdown?: string }) => {
// react-markdown lets us customize elements, so here we're using their example of replacing code blocks with SyntaxHighlighter. However when there are no language matches (` or ``` without a language specifier) then we default to a normal code element for inline code. Code blocks without a language specifier shouldn't be a common occurrence as we prompt Claude to always use a language specifier.
// when claude wraps text in thinking tags, he doesnt use line breaks so we need to insert those ourselves to render markdown correctly
const parsed = markdown?.replace(/<thinking>([\s\S]*?)<\/thinking>/g, (match, content) => {
return `_<thinking>_\n\n${content}\n\n_</thinking>_`
})
return (
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere" }}>
<ReactMarkdown
children={parsed}
components={{
p(props) {
const { style, ...rest } = props
return (
<p
style={{
...style,
margin: 0,
marginTop: 0,
marginBottom: 0,
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}
{...rest}
const Markdown = memo(({ markdown }: { markdown?: string }) => {
// react-markdown lets us customize elements, so here we're using their example of replacing code blocks with SyntaxHighlighter. However when there are no language matches (` or ``` without a language specifier) then we default to a normal code element for inline code. Code blocks without a language specifier shouldn't be a common occurrence as we prompt Claude to always use a language specifier.
// when claude wraps text in thinking tags, he doesnt use line breaks so we need to insert those ourselves to render markdown correctly
const parsed = markdown?.replace(/<thinking>([\s\S]*?)<\/thinking>/g, (match, content) => {
return `_<thinking>_\n\n${content}\n\n_</thinking>_`
})
return (
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere" }}>
<ReactMarkdown
children={parsed}
components={{
p(props) {
const { style, ...rest } = props
return (
<p
style={{
...style,
margin: 0,
marginTop: 0,
marginBottom: 0,
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}
{...rest}
/>
)
},
ol(props) {
const { style, ...rest } = props
return (
<ol
style={{
...style,
padding: "0 0 0 20px",
margin: "10px 0",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}
{...rest}
/>
)
},
ul(props) {
const { style, ...rest } = props
return (
<ul
style={{
...style,
padding: "0 0 0 20px",
margin: "10px 0",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}
{...rest}
/>
)
},
// https://github.com/remarkjs/react-markdown?tab=readme-ov-file#use-custom-components-syntax-highlight
code(props) {
const { children, className, node, ...rest } = props
const match = /language-(\w+)/.exec(className || "")
return match ? (
<div
style={{
borderRadius: 3,
border: "1px solid var(--vscode-sideBar-border)",
overflow: "hidden",
}}>
<CodeBlock
source={`${"```"}${match[1]}\n${String(children).replace(/\n$/, "")}\n${"```"}`}
/>
)
},
ol(props) {
const { style, ...rest } = props
return (
<ol
style={{
...style,
padding: "0 0 0 20px",
margin: "10px 0",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}
{...rest}
/>
)
},
ul(props) {
const { style, ...rest } = props
return (
<ul
style={{
...style,
padding: "0 0 0 20px",
margin: "10px 0",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}
{...rest}
/>
)
},
// https://github.com/remarkjs/react-markdown?tab=readme-ov-file#use-custom-components-syntax-highlight
code(props) {
const { children, className, node, ...rest } = props
const match = /language-(\w+)/.exec(className || "")
return match ? (
<SyntaxHighlighter
{...(rest as any)} // will be passed down to pre
PreTag="div"
children={String(children).replace(/\n$/, "")}
language={match[1]}
style={{
...syntaxHighlighterStyle,
'code[class*="language-"]': {
background: "var(--vscode-editor-background)",
},
'pre[class*="language-"]': {
background: "var(--vscode-editor-background)",
},
}}
customStyle={{
overflowX: "auto",
overflowY: "hidden",
maxWidth: "100%",
margin: 0,
padding: "10px",
// important to note that min-width: max-content is not required here how it is in CodeBlock.tsx
borderRadius: 3,
border: "1px solid var(--vscode-sideBar-border)",
fontSize: "var(--vscode-editor-font-size)",
lineHeight: "var(--vscode-editor-line-height)",
fontFamily: "var(--vscode-editor-font-family)",
}}
/>
) : (
<code
{...rest}
className={className}
style={{
whiteSpace: "pre-line",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
{children}
</code>
)
},
}}
/>
</div>
)
}
)
</div>
) : (
<code
{...rest}
className={className}
style={{
whiteSpace: "pre-line",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
{children}
</code>
)
},
}}
/>
</div>
)
})

View File

@@ -1,6 +1,5 @@
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
import vsDarkPlus from "react-syntax-highlighter/dist/esm/styles/prism/vsc-dark-plus"
import DynamicTextArea from "react-textarea-autosize"
import { useEvent, useMount } from "react-use"
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
@@ -9,7 +8,6 @@ import { combineApiRequests } from "../../../src/shared/combineApiRequests"
import { combineCommandSequences, COMMAND_STDIN_STRING } from "../../../src/shared/combineCommandSequences"
import { getApiMetrics } from "../../../src/shared/getApiMetrics"
import { useExtensionState } from "../context/ExtensionStateContext"
import { getSyntaxHighlighterStyleFromTheme } from "../utils/getSyntaxHighlighterStyleFromTheme"
import { vscode } from "../utils/vscode"
import Announcement from "./Announcement"
import ChatRow from "./ChatRow"
@@ -36,14 +34,7 @@ const ChatView = ({
hideAnnouncement,
showHistoryView,
}: ChatViewProps) => {
const {
version,
claudeMessages: messages,
taskHistory,
themeName: vscodeThemeName,
apiConfiguration,
uriScheme,
} = useExtensionState()
const { version, claudeMessages: messages, taskHistory, apiConfiguration, uriScheme } = useExtensionState()
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
const task = messages.length > 0 ? messages[0] : undefined // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see ClaudeDev.abort)
@@ -65,7 +56,6 @@ const ChatView = ({
const [enableButtons, setEnableButtons] = useState<boolean>(false)
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined)
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined)
const [syntaxHighlighterStyle, setSyntaxHighlighterStyle] = useState(vsDarkPlus)
const virtuosoRef = useRef<VirtuosoHandle>(null)
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
@@ -76,14 +66,6 @@ const ChatView = ({
}))
}
useEffect(() => {
if (!vscodeThemeName) return
const theme = getSyntaxHighlighterStyleFromTheme(vscodeThemeName)
if (theme) {
setSyntaxHighlighterStyle(theme)
}
}, [vscodeThemeName])
useEffect(() => {
// if last message is an ask, show user ask UI
@@ -486,7 +468,6 @@ const ChatView = ({
<ChatRow
key={message.ts}
message={message}
syntaxHighlighterStyle={syntaxHighlighterStyle}
isExpanded={expandedRows[message.ts] || false}
onToggleExpand={() => toggleRowExpansion(message.ts)}
lastModifiedMessage={modifiedMessages.at(-1)}
@@ -494,7 +475,7 @@ const ChatView = ({
handleSendStdin={handleSendStdin}
/>
),
[expandedRows, syntaxHighlighterStyle, modifiedMessages, visibleMessages.length, handleSendStdin]
[expandedRows, modifiedMessages, visibleMessages.length, handleSendStdin]
)
return (

View File

@@ -0,0 +1,85 @@
import { memo, useMemo } from "react"
import { getLanguageFromPath } from "../utils/getLanguageFromPath"
import CodeBlock from "./CodeBlock"
interface CodeAccordianProps {
code?: string
diff?: string
language?: string | undefined
path?: string
isExpanded: boolean
onToggleExpand: () => void
}
/*
We need to remove leading non-alphanumeric characters from the path in order for our leading ellipses trick to work.
^: Anchors the match to the start of the string.
[^a-zA-Z0-9]+: Matches one or more characters that are not alphanumeric.
The replace method removes these matched characters, effectively trimming the string up to the first alphanumeric character.
*/
const removeLeadingNonAlphanumeric = (path: string): string => path.replace(/^[^a-zA-Z0-9]+/, "")
const CodeAccordian = ({ code, diff, language, path, isExpanded, onToggleExpand }: CodeAccordianProps) => {
const inferredLanguage = useMemo(
() => code && (language ?? (path ? getLanguageFromPath(path) : undefined)),
[path, language, code]
)
return (
<div
style={{
borderRadius: 3,
backgroundColor: "var(--vscode-editor-background)",
overflow: "hidden", // This ensures the inner scrollable area doesn't overflow the rounded corners
border: "1px solid var(--vscode-editorGroup-border)",
}}>
{path && (
<div
style={{
color: "var(--vscode-descriptionForeground)",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "6px 10px",
cursor: "pointer",
}}
onClick={onToggleExpand}>
<span
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
marginRight: "8px",
fontSize: "11px",
// trick to get ellipsis at beginning of string
direction: "rtl",
textAlign: "left",
}}>
{removeLeadingNonAlphanumeric(path) + "\u200E"}
</span>
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
</div>
)}
{(!path || isExpanded) && (
<div
//className="code-block-scrollable" this doesn't seem to be necessary anymore, on silicon macs it shows the native mac scrollbar instead of the vscode styled one
style={{
overflowX: "auto",
overflowY: "hidden",
maxWidth: "100%",
}}>
<CodeBlock
source={`${"```"}${diff !== undefined ? "diff" : inferredLanguage}\n${(
code ??
diff ??
""
).trim()}\n${"```"}`}
/>
</div>
)}
</div>
)
}
// memo does shallow comparison of props, so if you need it to re-render when a nested object changes, you need to pass a custom comparison function
export default memo(CodeAccordian)

View File

@@ -1,171 +1,129 @@
import { memo, useMemo } from "react"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { getLanguageFromPath } from "../utils/getLanguageFromPath"
import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme"
import { memo, useEffect } from "react"
import { useRemark } from "react-remark"
import rehypeHighlight, { Options } from "rehype-highlight"
import styled from "styled-components"
import { visit } from "unist-util-visit"
import { useExtensionState } from "../context/ExtensionStateContext"
const BG_COLOR = "var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))"
/*
const vscodeSyntaxStyle: React.CSSProperties = {
backgroundColor: "var(--vscode-editor-background)",
color: "var(--vscode-editor-foreground)",
fontFamily: "var(--vscode-editor-font-family)",
fontSize: "var(--vscode-editor-font-size)",
lineHeight: "var(--vscode-editor-line-height)",
textAlign: "left",
whiteSpace: "pre",
wordSpacing: "normal",
wordBreak: "normal",
wordWrap: "normal",
tabSize: 4,
hyphens: "none",
padding: "1em",
margin: "0.5em 0",
overflow: "auto",
borderRadius: "6px",
}
const tokenStyles = {
comment: { color: "var(--vscode-editor-foreground)" },
prolog: { color: "var(--vscode-editor-foreground)" },
doctype: { color: "var(--vscode-editor-foreground)" },
cdata: { color: "var(--vscode-editor-foreground)" },
punctuation: { color: "var(--vscode-editor-foreground)" },
property: { color: "var(--vscode-symbolIcon-propertyForeground)" },
tag: { color: "var(--vscode-symbolIcon-colorForeground)" },
boolean: { color: "var(--vscode-symbolIcon-booleanForeground)" },
number: { color: "var(--vscode-symbolIcon-numberForeground)" },
constant: { color: "var(--vscode-symbolIcon-constantForeground)" },
symbol: { color: "var(--vscode-symbolIcon-colorForeground)" },
selector: { color: "var(--vscode-symbolIcon-colorForeground)" },
"attr-name": { color: "var(--vscode-symbolIcon-propertyForeground)" },
string: { color: "var(--vscode-symbolIcon-stringForeground)" },
char: { color: "var(--vscode-symbolIcon-stringForeground)" },
builtin: { color: "var(--vscode-symbolIcon-keywordForeground)" },
inserted: { color: "var(--vscode-gitDecoration-addedResourceForeground)" },
operator: { color: "var(--vscode-symbolIcon-operatorForeground)" },
entity: { color: "var(--vscode-symbolIcon-snippetForeground)", cursor: "help" },
url: { color: "var(--vscode-textLink-foreground)" },
variable: { color: "var(--vscode-symbolIcon-variableForeground)" },
atrule: { color: "var(--vscode-symbolIcon-keywordForeground)" },
"attr-value": { color: "var(--vscode-symbolIcon-stringForeground)" },
keyword: { color: "var(--vscode-symbolIcon-keywordForeground)" },
function: { color: "var(--vscode-symbolIcon-functionForeground)" },
regex: { color: "var(--vscode-symbolIcon-regexForeground)" },
important: { color: "var(--vscode-editorWarning-foreground)", fontWeight: "bold" },
bold: { fontWeight: "bold" },
italic: { fontStyle: "italic" },
deleted: { color: "var(--vscode-gitDecoration-deletedResourceForeground)" },
}
overflowX: auto + inner div with padding results in an issue where the top/left/bottom padding renders but the right padding inside does not count as overflow as the width of the element is not exceeded. Once the inner div is outside the boundaries of the parent it counts as overflow.
https://stackoverflow.com/questions/60778406/why-is-padding-right-clipped-with-overflowscroll/77292459#77292459
this fixes the issue of right padding clipped off
“ideal” size in a given axis when given infinite available space--allows the syntax highlighter to grow to largest possible width including its padding
minWidth: "max-content",
*/
interface CodeBlockProps {
code?: string
diff?: string
language?: string | undefined
path?: string
syntaxHighlighterStyle: SyntaxHighlighterStyle
isExpanded: boolean
onToggleExpand: () => void
}
const StyledMarkdown = styled.div`
pre {
background-color: ${BG_COLOR};
border-radius: 5px;
margin: 0;
min-width: max-content;
padding: 10px 10px;
}
/*
We need to remove leading non-alphanumeric characters from the path in order for our leading ellipses trick to work.
^: Anchors the match to the start of the string.
[^a-zA-Z0-9]+: Matches one or more characters that are not alphanumeric.
The replace method removes these matched characters, effectively trimming the string up to the first alphanumeric character.
*/
const removeLeadingNonAlphanumeric = (path: string): string => path.replace(/^[^a-zA-Z0-9]+/, "")
pre > code {
.hljs-deletion {
background-color: var(--vscode-diffEditor-removedTextBackground);
}
.hljs-addition {
background-color: var(--vscode-diffEditor-insertedTextBackground);
}
}
const CodeBlock = ({
code,
diff,
language,
path,
syntaxHighlighterStyle,
isExpanded,
onToggleExpand,
}: CodeBlockProps) => {
const inferredLanguage = useMemo(
() => code && (language ?? (path ? getLanguageFromPath(path) : undefined)),
[path, language, code]
)
code {
span.line:empty {
display: none;
}
word-wrap: break-word;
border-radius: 5px;
background-color: ${BG_COLOR};
font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px));
font-family: var(--vscode-editor-font-family);
}
code:not(pre > code) {
font-family: var(--vscode-editor-font-family);
color: #f78383;
}
background-color: ${BG_COLOR};
font-family: var(--vscode-font-family), system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px));
color: var(--vscode-editor-foreground, #fff);
p,
li,
ol,
ul {
line-height: 1.5;
}
`
const StyledPre = styled.pre<{ theme: any }>`
& .hljs {
color: var(--vscode-editor-foreground, #fff);
}
${(props) =>
Object.keys(props.theme)
.map((key, index) => {
return `
& ${key} {
color: ${props.theme[key]};
}
`
})
.join("")}
`
const CodeBlock = memo(function CodeBlock({ source }: { source?: string }) {
const { theme } = useExtensionState()
const [reactContent, setMarkdownSource] = useRemark({
remarkPlugins: [
() => {
return (tree) => {
visit(tree, "code", (node: any) => {
if (!node.lang) {
node.lang = "javascript"
} else if (node.lang.includes(".")) {
// if the langauge is a file, get the extension
node.lang = node.lang.split(".").slice(-1)[0]
}
})
}
},
],
rehypePlugins: [
rehypeHighlight as any,
{
// languages: {},
} as Options,
],
rehypeReactOptions: {
components: {
pre: ({ node, ...preProps }: any) => <StyledPre {...preProps} theme={theme} />,
},
},
})
useEffect(() => {
setMarkdownSource(source || "")
}, [source, setMarkdownSource, theme])
return (
<div
style={{
borderRadius: "3px",
backgroundColor: "var(--vscode-editor-background)",
overflow: "hidden", // This ensures the inner scrollable area doesn't overflow the rounded corners
border: "1px solid var(--vscode-editorGroup-border)",
overflowY: "auto",
maxHeight: "100%",
backgroundColor: BG_COLOR,
}}>
{path && (
<div
style={{
color: "var(--vscode-descriptionForeground)",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "6px 10px",
cursor: "pointer",
}}
onClick={onToggleExpand}>
<span
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
marginRight: "8px",
fontSize: "11px",
// trick to get ellipsis at beginning of string
direction: "rtl",
textAlign: "left",
}}>
{removeLeadingNonAlphanumeric(path) + "\u200E"}
</span>
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
</div>
)}
{(!path || isExpanded) && (
<div
//className="code-block-scrollable" this doesn't seem to be necessary anymore, on silicon macs it shows the native mac scrollbar instead of the vscode styled one
style={{
overflowX: "auto",
overflowY: "hidden",
maxWidth: "100%",
}}>
<SyntaxHighlighter
wrapLines={false}
language={diff ? "diff" : inferredLanguage} // "diff" automatically colors changed lines in green/red
style={{
...syntaxHighlighterStyle,
// Our syntax highlighter style doesn't always match the vscode theme 1:1, so we'll apply sensible styles here that vscode exposes to us
'code[class*="language-"]': {
background: "var(--vscode-editor-background)",
},
'pre[class*="language-"]': {
background: "var(--vscode-editor-background)",
},
}}
customStyle={{
margin: 0,
padding: "6px 10px",
/*
overflowX: auto + inner div with padding results in an issue where the top/left/bottom padding renders but the right padding inside does not count as overflow as the width of the element is not exceeded. Once the inner div is outside the boundaries of the parent it counts as overflow.
https://stackoverflow.com/questions/60778406/why-is-padding-right-clipped-with-overflowscroll/77292459#77292459
this fixes the issue of right padding clipped off
“ideal” size in a given axis when given infinite available space--allows the syntax highlighter to grow to largest possible width including its padding
*/
minWidth: "max-content",
borderRadius: 0,
fontSize: "var(--vscode-editor-font-size)",
lineHeight: "var(--vscode-editor-line-height)",
fontFamily: "var(--vscode-editor-font-family)",
}}>
{code ?? diff ?? ""}
</SyntaxHighlighter>
</div>
)}
<StyledMarkdown>{reactContent}</StyledMarkdown>
</div>
)
}
// memo does shallow comparison of props, so if you need it to re-render when a nested object changes, you need to pass a custom comparison function
export default memo(CodeBlock)
})
export default CodeBlock