|
|
@@ -1,4 +1,4 @@
|
|
|
-import React, { memo } from "react"
|
|
|
+import React, { memo, useMemo } from "react"
|
|
|
import ReactMarkdown from "react-markdown"
|
|
|
import styled from "styled-components"
|
|
|
import { visit } from "unist-util-visit"
|
|
|
@@ -7,7 +7,6 @@ import remarkMath from "remark-math"
|
|
|
import remarkGfm from "remark-gfm"
|
|
|
|
|
|
import { vscode } from "@src/utils/vscode"
|
|
|
-import { useExtensionState } from "@src/context/ExtensionStateContext"
|
|
|
|
|
|
import CodeBlock from "./CodeBlock"
|
|
|
import MermaidBlock from "./MermaidBlock"
|
|
|
@@ -117,6 +116,19 @@ const StyledMarkdown = styled.div`
|
|
|
|
|
|
p {
|
|
|
white-space: pre-wrap;
|
|
|
+ margin: 0.5em 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Prevent layout shifts during streaming */
|
|
|
+ pre {
|
|
|
+ min-height: 3em;
|
|
|
+ transition: height 0.2s ease-out;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Code block container styling */
|
|
|
+ div:has(> pre) {
|
|
|
+ position: relative;
|
|
|
+ contain: layout style;
|
|
|
}
|
|
|
|
|
|
a {
|
|
|
@@ -133,11 +145,18 @@ const StyledMarkdown = styled.div`
|
|
|
|
|
|
/* Table styles for remark-gfm */
|
|
|
table {
|
|
|
- width: 100%;
|
|
|
border-collapse: collapse;
|
|
|
margin: 1em 0;
|
|
|
+ width: auto;
|
|
|
+ min-width: 50%;
|
|
|
+ max-width: 100%;
|
|
|
+ table-layout: fixed;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Table wrapper for horizontal scrolling */
|
|
|
+ .table-wrapper {
|
|
|
overflow-x: auto;
|
|
|
- display: block;
|
|
|
+ margin: 1em 0;
|
|
|
}
|
|
|
|
|
|
th,
|
|
|
@@ -145,6 +164,8 @@ const StyledMarkdown = styled.div`
|
|
|
border: 1px solid var(--vscode-panel-border);
|
|
|
padding: 8px 12px;
|
|
|
text-align: left;
|
|
|
+ word-wrap: break-word;
|
|
|
+ overflow-wrap: break-word;
|
|
|
}
|
|
|
|
|
|
th {
|
|
|
@@ -163,96 +184,104 @@ const StyledMarkdown = styled.div`
|
|
|
`
|
|
|
|
|
|
const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
|
|
|
- const { theme: _theme } = useExtensionState()
|
|
|
+ const components = useMemo(
|
|
|
+ () => ({
|
|
|
+ table: ({ children, ...props }: any) => {
|
|
|
+ return (
|
|
|
+ <div className="table-wrapper">
|
|
|
+ <table {...props}>{children}</table>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ },
|
|
|
+ a: ({ href, children, ...props }: any) => {
|
|
|
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
|
+ // Only process file:// protocol or local file paths
|
|
|
+ const isLocalPath = href?.startsWith("file://") || href?.startsWith("/") || !href?.includes("://")
|
|
|
+
|
|
|
+ if (!isLocalPath) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ e.preventDefault()
|
|
|
+
|
|
|
+ // Handle absolute vs project-relative paths
|
|
|
+ let filePath = href.replace("file://", "")
|
|
|
+
|
|
|
+ // Extract line number if present
|
|
|
+ const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
|
|
|
+ let values = undefined
|
|
|
+ if (match) {
|
|
|
+ filePath = match[1]
|
|
|
+ values = { line: parseInt(match[2]) }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add ./ prefix if needed
|
|
|
+ if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
|
|
|
+ filePath = "./" + filePath
|
|
|
+ }
|
|
|
+
|
|
|
+ vscode.postMessage({
|
|
|
+ type: "openFile",
|
|
|
+ text: filePath,
|
|
|
+ values,
|
|
|
+ })
|
|
|
+ }
|
|
|
|
|
|
- const components = {
|
|
|
- a: ({ href, children, ...props }: any) => {
|
|
|
- const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
|
- // Only process file:// protocol or local file paths
|
|
|
- const isLocalPath = href?.startsWith("file://") || href?.startsWith("/") || !href?.includes("://")
|
|
|
+ return (
|
|
|
+ <a {...props} href={href} onClick={handleClick}>
|
|
|
+ {children}
|
|
|
+ </a>
|
|
|
+ )
|
|
|
+ },
|
|
|
+ pre: ({ children, ..._props }: any) => {
|
|
|
+ // The structure from react-markdown v9 is: pre > code > text
|
|
|
+ const codeEl = children as React.ReactElement
|
|
|
|
|
|
- if (!isLocalPath) {
|
|
|
- return
|
|
|
+ if (!codeEl || !codeEl.props) {
|
|
|
+ return <pre>{children}</pre>
|
|
|
}
|
|
|
|
|
|
- e.preventDefault()
|
|
|
-
|
|
|
- // Handle absolute vs project-relative paths
|
|
|
- let filePath = href.replace("file://", "")
|
|
|
+ const { className = "", children: codeChildren } = codeEl.props
|
|
|
|
|
|
- // Extract line number if present
|
|
|
- const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
|
|
|
- let values = undefined
|
|
|
- if (match) {
|
|
|
- filePath = match[1]
|
|
|
- values = { line: parseInt(match[2]) }
|
|
|
+ // Get the actual code text
|
|
|
+ let codeString = ""
|
|
|
+ if (typeof codeChildren === "string") {
|
|
|
+ codeString = codeChildren
|
|
|
+ } else if (Array.isArray(codeChildren)) {
|
|
|
+ codeString = codeChildren.filter((child) => typeof child === "string").join("")
|
|
|
}
|
|
|
|
|
|
- // Add ./ prefix if needed
|
|
|
- if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
|
|
|
- filePath = "./" + filePath
|
|
|
+ // Handle mermaid diagrams
|
|
|
+ if (className.includes("language-mermaid")) {
|
|
|
+ return (
|
|
|
+ <div style={{ margin: "1em 0" }}>
|
|
|
+ <MermaidBlock code={codeString} />
|
|
|
+ </div>
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
- vscode.postMessage({
|
|
|
- type: "openFile",
|
|
|
- text: filePath,
|
|
|
- values,
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- return (
|
|
|
- <a {...props} href={href} onClick={handleClick}>
|
|
|
- {children}
|
|
|
- </a>
|
|
|
- )
|
|
|
- },
|
|
|
- pre: ({ children, ..._props }: any) => {
|
|
|
- // The structure from react-markdown v9 is: pre > code > text
|
|
|
- const codeEl = children as React.ReactElement
|
|
|
-
|
|
|
- if (!codeEl || !codeEl.props) {
|
|
|
- return <pre>{children}</pre>
|
|
|
- }
|
|
|
-
|
|
|
- const { className = "", children: codeChildren } = codeEl.props
|
|
|
-
|
|
|
- // Get the actual code text
|
|
|
- let codeString = ""
|
|
|
- if (typeof codeChildren === "string") {
|
|
|
- codeString = codeChildren
|
|
|
- } else if (Array.isArray(codeChildren)) {
|
|
|
- codeString = codeChildren.filter((child) => typeof child === "string").join("")
|
|
|
- }
|
|
|
-
|
|
|
- // Handle mermaid diagrams
|
|
|
- if (className.includes("language-mermaid")) {
|
|
|
+ // Extract language from className
|
|
|
+ const match = /language-(\w+)/.exec(className)
|
|
|
+ const language = match ? match[1] : "text"
|
|
|
+
|
|
|
+ // Wrap CodeBlock in a div to ensure proper separation
|
|
|
return (
|
|
|
<div style={{ margin: "1em 0" }}>
|
|
|
- <MermaidBlock code={codeString} />
|
|
|
+ <CodeBlock source={codeString} language={language} />
|
|
|
</div>
|
|
|
)
|
|
|
- }
|
|
|
-
|
|
|
- // Extract language from className
|
|
|
- const match = /language-(\w+)/.exec(className)
|
|
|
- const language = match ? match[1] : "text"
|
|
|
-
|
|
|
- // Wrap CodeBlock in a div to ensure proper separation
|
|
|
- return (
|
|
|
- <div style={{ margin: "1em 0" }}>
|
|
|
- <CodeBlock source={codeString} language={language} />
|
|
|
- </div>
|
|
|
- )
|
|
|
- },
|
|
|
- code: ({ children, className, ...props }: any) => {
|
|
|
- // This handles inline code
|
|
|
- return (
|
|
|
- <code className={className} {...props}>
|
|
|
- {children}
|
|
|
- </code>
|
|
|
- )
|
|
|
- },
|
|
|
- }
|
|
|
+ },
|
|
|
+ code: ({ children, className, ...props }: any) => {
|
|
|
+ // This handles inline code
|
|
|
+ return (
|
|
|
+ <code className={className} {...props}>
|
|
|
+ {children}
|
|
|
+ </code>
|
|
|
+ )
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ [],
|
|
|
+ )
|
|
|
|
|
|
return (
|
|
|
<StyledMarkdown>
|