11import { For , Show , Suspense , createEffect , createMemo , createSignal , lazy , type Accessor , type Component , type JSX } from "solid-js"
22import type { FileNode } from "@opencode-ai/sdk/v2/client"
33
4- import { Copy , RefreshCw , Save , Search } from "lucide-solid"
4+ import { Copy , RefreshCw , Save , Search , WrapText } from "lucide-solid"
55
66import SplitFilePanel from "../components/SplitFilePanel"
7+ import { Markdown } from "../../../../markdown"
78import { copyToClipboard } from "../../../../../lib/clipboard"
89import { showToastNotification } from "../../../../../lib/notifications"
10+ import { useTheme } from "../../../../../lib/theme"
911
1012const LazyMonacoFileViewer = lazy ( ( ) =>
1113 import ( "../../../../file-viewer/monaco-file-viewer" ) . then ( ( module ) => ( { default : module . MonacoFileViewer } ) ) ,
1214)
1315
16+ function isMarkdownPath ( path : string | null | undefined ) : boolean {
17+ if ( ! path ) return false
18+ return / \. ( m d | m a r k d o w n | m d o w n | m k d n ) $ / i. test ( path )
19+ }
20+
1421interface FilesTabProps {
1522 t : ( key : string , vars ?: Record < string , any > ) => string
1623
@@ -25,6 +32,7 @@ interface FilesTabProps {
2532 browserSelectedError : Accessor < string | null >
2633 browserSelectedDirty : Accessor < boolean >
2734 browserSelectedSaving : Accessor < boolean >
35+ wordWrapMode : Accessor < "on" | "off" >
2836
2937 parentPath : Accessor < string | null >
3038 scopeKey : Accessor < string >
@@ -34,6 +42,7 @@ interface FilesTabProps {
3442 onRefresh : ( ) => void
3543 onSave : ( content : string ) => void
3644 onContentChange : ( content : string ) => void
45+ onWordWrapModeChange : ( mode : "on" | "off" ) => void
3746
3847 listOpen : Accessor < boolean >
3948 onToggleList : ( ) => void
@@ -45,6 +54,9 @@ interface FilesTabProps {
4554
4655const FilesTab : Component < FilesTabProps > = ( props ) => {
4756 const [ filterQuery , setFilterQuery ] = createSignal ( "" )
57+ const { isDark } = useTheme ( )
58+ const [ markdownPreviewEnabled , setMarkdownPreviewEnabled ] = createSignal ( false )
59+ let markdownPreviewRef : HTMLDivElement | undefined
4860
4961 createEffect ( ( ) => {
5062 props . browserPath ( )
@@ -78,6 +90,14 @@ const FilesTab: Component<FilesTabProps> = (props) => {
7890 const listEmptyMessage = ( ) =>
7991 normalizedQuery ( ) ? props . t ( "instanceShell.filesShell.search.empty" ) : props . t ( "instanceShell.filesShell.listEmpty" )
8092
93+ const selectedMarkdownFile = createMemo ( ( ) => isMarkdownPath ( props . browserSelectedPath ( ) ) )
94+ const showingMarkdownPreview = createMemo ( ( ) => selectedMarkdownFile ( ) && markdownPreviewEnabled ( ) )
95+
96+ createEffect ( ( ) => {
97+ if ( ! selectedMarkdownFile ( ) ) {
98+ setMarkdownPreviewEnabled ( false )
99+ }
100+ } )
81101 const handleSave = ( ) => {
82102 const content = props . browserSelectedContent ( )
83103 if ( content !== undefined && content !== null ) {
@@ -94,6 +114,11 @@ const FilesTab: Component<FilesTabProps> = (props) => {
94114 } )
95115 }
96116
117+ createEffect ( ( ) => {
118+ if ( ! showingMarkdownPreview ( ) ) return
119+ requestAnimationFrame ( ( ) => markdownPreviewRef ?. focus ( ) )
120+ } )
121+
97122 const FileList : Component = ( ) => (
98123 < >
99124 < div class = "px-2 py-2 border-b border-base" >
@@ -182,6 +207,13 @@ const FilesTab: Component<FilesTabProps> = (props) => {
182207 </ >
183208 )
184209
210+ const handleMarkdownPreviewKeyDown = ( event : KeyboardEvent ) => {
211+ if ( ! ( event . ctrlKey || event . metaKey ) || event . key . toLowerCase ( ) !== "s" ) return
212+ if ( props . browserSelectedSaving ( ) || ! props . browserSelectedDirty ( ) ) return
213+ event . preventDefault ( )
214+ handleSave ( )
215+ }
216+
185217 const renderContent = ( ) : JSX . Element => {
186218 const headerDisplayedPath = ( ) => props . browserSelectedPath ( ) || props . browserPath ( )
187219
@@ -192,7 +224,7 @@ const FilesTab: Component<FilesTabProps> = (props) => {
192224
193225 const renderViewer = ( ) => (
194226 < div class = "file-viewer-panel flex-1" >
195- < div class = "file-viewer-content file-viewer-content-- monaco" >
227+ < div class = { showingMarkdownPreview ( ) ? "file-viewer-content" : " file-viewer-content file-viewer-content-- monaco"} >
196228 < Show
197229 when = { props . browserSelectedLoading ( ) }
198230 fallback = {
@@ -212,21 +244,37 @@ const FilesTab: Component<FilesTabProps> = (props) => {
212244 }
213245 >
214246 { ( payload ) => (
215- < Suspense
247+ < Show
248+ when = { showingMarkdownPreview ( ) }
216249 fallback = {
217- < div class = "file-viewer-empty" >
218- < span class = "file-viewer-empty-text" > { props . t ( "instanceInfo.loading" ) } </ span >
219- </ div >
250+ < Suspense
251+ fallback = {
252+ < div class = "file-viewer-empty" >
253+ < span class = "file-viewer-empty-text" > { props . t ( "instanceInfo.loading" ) } </ span >
254+ </ div >
255+ }
256+ >
257+ < LazyMonacoFileViewer
258+ scopeKey = { props . scopeKey ( ) }
259+ path = { payload ( ) . path }
260+ content = { payload ( ) . content }
261+ wordWrap = { props . wordWrapMode ( ) }
262+ onSave = { props . onSave }
263+ onContentChange = { props . onContentChange }
264+ />
265+ </ Suspense >
220266 }
221267 >
222- < LazyMonacoFileViewer
223- scopeKey = { props . scopeKey ( ) }
224- path = { payload ( ) . path }
225- content = { payload ( ) . content }
226- onSave = { props . onSave }
227- onContentChange = { props . onContentChange }
228- />
229- </ Suspense >
268+ < div
269+ ref = { markdownPreviewRef }
270+ class = "h-full outline-none"
271+ tabIndex = { 0 }
272+ onKeyDown = { handleMarkdownPreviewKeyDown }
273+ onMouseDown = { ( ) => markdownPreviewRef ?. focus ( ) }
274+ >
275+ < Markdown part = { { type : "text" , text : payload ( ) . content } } isDark = { isDark ( ) } escapeRawHtml />
276+ </ div >
277+ </ Show >
230278 ) }
231279 </ Show >
232280 }
@@ -262,13 +310,33 @@ const FilesTab: Component<FilesTabProps> = (props) => {
262310 </ Show >
263311 < Show when = { props . browserError ( ) } > { ( err ) => < span class = "text-error" > { err ( ) } </ span > } </ Show >
264312 </ div >
313+ < button
314+ type = "button"
315+ class = { `file-viewer-toolbar-button${ showingMarkdownPreview ( ) ? " active" : "" } ` }
316+ disabled = { ! selectedMarkdownFile ( ) }
317+ style = { { "margin-inline-start" : "auto" } }
318+ onClick = { ( ) => selectedMarkdownFile ( ) && setMarkdownPreviewEnabled ( ( prev ) => ! prev ) }
319+ >
320+ { showingMarkdownPreview ( )
321+ ? props . t ( "instanceShell.filesShell.showSource" )
322+ : props . t ( "instanceShell.filesShell.previewMarkdown" ) }
323+ </ button >
324+ < button
325+ type = "button"
326+ class = { `file-viewer-toolbar-icon-button${ props . wordWrapMode ( ) === "on" ? " active" : "" } ` }
327+ title = { props . wordWrapMode ( ) === "on" ? props . t ( "instanceShell.filesShell.disableWordWrap" ) : props . t ( "instanceShell.filesShell.enableWordWrap" ) }
328+ aria-label = { props . wordWrapMode ( ) === "on" ? props . t ( "instanceShell.filesShell.disableWordWrap" ) : props . t ( "instanceShell.filesShell.enableWordWrap" ) }
329+ disabled = { showingMarkdownPreview ( ) }
330+ onClick = { ( ) => props . onWordWrapModeChange ( props . wordWrapMode ( ) === "on" ? "off" : "on" ) }
331+ >
332+ < WrapText class = "h-4 w-4" />
333+ </ button >
265334 < button
266335 type = "button"
267336 class = "files-header-icon-button"
268337 title = { props . t ( "instanceShell.rightPanel.actions.save" ) || "Save (Ctrl+S)" }
269338 aria-label = { props . t ( "instanceShell.rightPanel.actions.save" ) || "Save" }
270339 disabled = { props . browserSelectedSaving ( ) || ! props . browserSelectedDirty ( ) }
271- style = { { "margin-inline-start" : "auto" } }
272340 onClick = { handleSave }
273341 >
274342 < Show when = { props . browserSelectedSaving ( ) } fallback = { < Save class = "h-4 w-4" /> } >
0 commit comments