1- import React , { memo } from "react"
1+ import React , { memo , useMemo } from "react"
22import ReactMarkdown from "react-markdown"
33import styled from "styled-components"
44import { visit } from "unist-util-visit"
@@ -7,7 +7,6 @@ import remarkMath from "remark-math"
77import remarkGfm from "remark-gfm"
88
99import { vscode } from "@src/utils/vscode"
10- import { useExtensionState } from "@src/context/ExtensionStateContext"
1110
1211import CodeBlock from "./CodeBlock"
1312import MermaidBlock from "./MermaidBlock"
@@ -117,6 +116,19 @@ const StyledMarkdown = styled.div`
117116
118117 p {
119118 white-space: pre-wrap;
119+ margin: 0.5em 0;
120+ }
121+
122+ /* Prevent layout shifts during streaming */
123+ pre {
124+ min-height: 3em;
125+ transition: height 0.2s ease-out;
126+ }
127+
128+ /* Code block container styling */
129+ div:has(> pre) {
130+ position: relative;
131+ contain: layout style;
120132 }
121133
122134 a {
@@ -133,18 +145,27 @@ const StyledMarkdown = styled.div`
133145
134146 /* Table styles for remark-gfm */
135147 table {
136- width: 100%;
137148 border-collapse: collapse;
138149 margin: 1em 0;
150+ width: auto;
151+ min-width: 50%;
152+ max-width: 100%;
153+ table-layout: fixed;
154+ }
155+
156+ /* Table wrapper for horizontal scrolling */
157+ .table-wrapper {
139158 overflow-x: auto;
140- display: block ;
159+ margin: 1em 0 ;
141160 }
142161
143162 th,
144163 td {
145164 border: 1px solid var(--vscode-panel-border);
146165 padding: 8px 12px;
147166 text-align: left;
167+ word-wrap: break-word;
168+ overflow-wrap: break-word;
148169 }
149170
150171 th {
@@ -163,96 +184,104 @@ const StyledMarkdown = styled.div`
163184`
164185
165186const MarkdownBlock = memo ( ( { markdown } : MarkdownBlockProps ) => {
166- const { theme : _theme } = useExtensionState ( )
187+ const components = useMemo (
188+ ( ) => ( {
189+ table : ( { children, ...props } : any ) => {
190+ return (
191+ < div className = "table-wrapper" >
192+ < table { ...props } > { children } </ table >
193+ </ div >
194+ )
195+ } ,
196+ a : ( { href, children, ...props } : any ) => {
197+ const handleClick = ( e : React . MouseEvent < HTMLAnchorElement > ) => {
198+ // Only process file:// protocol or local file paths
199+ const isLocalPath = href ?. startsWith ( "file://" ) || href ?. startsWith ( "/" ) || ! href ?. includes ( "://" )
200+
201+ if ( ! isLocalPath ) {
202+ return
203+ }
204+
205+ e . preventDefault ( )
206+
207+ // Handle absolute vs project-relative paths
208+ let filePath = href . replace ( "file://" , "" )
209+
210+ // Extract line number if present
211+ const match = filePath . match ( / ( .* ) : ( \d + ) ( - \d + ) ? $ / )
212+ let values = undefined
213+ if ( match ) {
214+ filePath = match [ 1 ]
215+ values = { line : parseInt ( match [ 2 ] ) }
216+ }
217+
218+ // Add ./ prefix if needed
219+ if ( ! filePath . startsWith ( "/" ) && ! filePath . startsWith ( "./" ) ) {
220+ filePath = "./" + filePath
221+ }
222+
223+ vscode . postMessage ( {
224+ type : "openFile" ,
225+ text : filePath ,
226+ values,
227+ } )
228+ }
167229
168- const components = {
169- a : ( { href, children, ...props } : any ) => {
170- const handleClick = ( e : React . MouseEvent < HTMLAnchorElement > ) => {
171- // Only process file:// protocol or local file paths
172- const isLocalPath = href ?. startsWith ( "file://" ) || href ?. startsWith ( "/" ) || ! href ?. includes ( "://" )
230+ return (
231+ < a { ...props } href = { href } onClick = { handleClick } >
232+ { children }
233+ </ a >
234+ )
235+ } ,
236+ pre : ( { children, ..._props } : any ) => {
237+ // The structure from react-markdown v9 is: pre > code > text
238+ const codeEl = children as React . ReactElement
173239
174- if ( ! isLocalPath ) {
175- return
240+ if ( ! codeEl || ! codeEl . props ) {
241+ return < pre > { children } </ pre >
176242 }
177243
178- e . preventDefault ( )
179-
180- // Handle absolute vs project-relative paths
181- let filePath = href . replace ( "file://" , "" )
244+ const { className = "" , children : codeChildren } = codeEl . props
182245
183- // Extract line number if present
184- const match = filePath . match ( / ( . * ) : ( \d + ) ( - \d + ) ? $ / )
185- let values = undefined
186- if ( match ) {
187- filePath = match [ 1 ]
188- values = { line : parseInt ( match [ 2 ] ) }
246+ // Get the actual code text
247+ let codeString = ""
248+ if ( typeof codeChildren === "string" ) {
249+ codeString = codeChildren
250+ } else if ( Array . isArray ( codeChildren ) ) {
251+ codeString = codeChildren . filter ( ( child ) => typeof child === "string" ) . join ( "" )
189252 }
190253
191- // Add ./ prefix if needed
192- if ( ! filePath . startsWith ( "/" ) && ! filePath . startsWith ( "./" ) ) {
193- filePath = "./" + filePath
254+ // Handle mermaid diagrams
255+ if ( className . includes ( "language-mermaid" ) ) {
256+ return (
257+ < div style = { { margin : "1em 0" } } >
258+ < MermaidBlock code = { codeString } />
259+ </ div >
260+ )
194261 }
195262
196- vscode . postMessage ( {
197- type : "openFile" ,
198- text : filePath ,
199- values,
200- } )
201- }
202-
203- return (
204- < a { ...props } href = { href } onClick = { handleClick } >
205- { children }
206- </ a >
207- )
208- } ,
209- pre : ( { children, ..._props } : any ) => {
210- // The structure from react-markdown v9 is: pre > code > text
211- const codeEl = children as React . ReactElement
212-
213- if ( ! codeEl || ! codeEl . props ) {
214- return < pre > { children } </ pre >
215- }
216-
217- const { className = "" , children : codeChildren } = codeEl . props
218-
219- // Get the actual code text
220- let codeString = ""
221- if ( typeof codeChildren === "string" ) {
222- codeString = codeChildren
223- } else if ( Array . isArray ( codeChildren ) ) {
224- codeString = codeChildren . filter ( ( child ) => typeof child === "string" ) . join ( "" )
225- }
226-
227- // Handle mermaid diagrams
228- if ( className . includes ( "language-mermaid" ) ) {
263+ // Extract language from className
264+ const match = / l a n g u a g e - ( \w + ) / . exec ( className )
265+ const language = match ? match [ 1 ] : "text"
266+
267+ // Wrap CodeBlock in a div to ensure proper separation
229268 return (
230269 < div style = { { margin : "1em 0" } } >
231- < MermaidBlock code = { codeString } />
270+ < CodeBlock source = { codeString } language = { language } />
232271 </ div >
233272 )
234- }
235-
236- // Extract language from className
237- const match = / l a n g u a g e - ( \w + ) / . exec ( className )
238- const language = match ? match [ 1 ] : "text"
239-
240- // Wrap CodeBlock in a div to ensure proper separation
241- return (
242- < div style = { { margin : "1em 0" } } >
243- < CodeBlock source = { codeString } language = { language } />
244- </ div >
245- )
246- } ,
247- code : ( { children, className, ...props } : any ) => {
248- // This handles inline code
249- return (
250- < code className = { className } { ...props } >
251- { children }
252- </ code >
253- )
254- } ,
255- }
273+ } ,
274+ code : ( { children, className, ...props } : any ) => {
275+ // This handles inline code
276+ return (
277+ < code className = { className } { ...props } >
278+ { children }
279+ </ code >
280+ )
281+ } ,
282+ } ) ,
283+ [ ] ,
284+ )
256285
257286 return (
258287 < StyledMarkdown >
0 commit comments