From f9c966d9474fa931d1c31d6ac9871d93fc4d731f Mon Sep 17 00:00:00 2001 From: Devin Date: Wed, 1 Apr 2026 13:55:57 -0700 Subject: [PATCH 1/8] update filesystem component --- package.json | 2 +- scripts/copy-static-assets.mjs | 36 +- src/components/filesystem/CodeEditorPane.tsx | 287 ++------ src/components/filesystem/FileTree.tsx | 132 +++- src/components/filesystem/FileWorkspace.tsx | 622 +++++------------- src/components/filesystem/monaco-loader.ts | 135 ---- src/components/filesystem/types.ts | 10 +- src/index.ts | 30 + src/styles/filesystem.css | 477 ++++++++------ tests/visual/harness/registry.jsx | 4 + .../filesystem-workspace.scenario.jsx | 16 +- .../hyperbrowser-file-workspace.scenario.jsx | 23 +- yarn.lock | 5 - 13 files changed, 656 insertions(+), 1123 deletions(-) delete mode 100644 src/components/filesystem/monaco-loader.ts diff --git a/package.json b/package.json index 8f0655a..b922477 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "default": "./dist/esm/index.js" }, "./styles.css": "./dist/styles/styles.css", + "./filesystem.css": "./dist/styles/filesystem.css", "./terminal-core.css": "./dist/styles/terminal-core.css", "./terminal.css": "./dist/styles/terminal.css", "./package.json": "./package.json" @@ -44,7 +45,6 @@ "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "hls.js": "^1.6.13", - "monaco-editor": "^0.52.2", "react-vnc": "^3.2.0" }, "devDependencies": { diff --git a/scripts/copy-static-assets.mjs b/scripts/copy-static-assets.mjs index 11b9dcf..cb4c453 100644 --- a/scripts/copy-static-assets.mjs +++ b/scripts/copy-static-assets.mjs @@ -11,6 +11,12 @@ const sourceTerminalCoreStylesPath = path.join( "terminal-core.css" ); const sourceTerminalStylesPath = path.join(rootDir, "src", "styles", "terminal.css"); +const sourceFilesystemStylesPath = path.join( + rootDir, + "src", + "styles", + "filesystem.css" +); const xtermStylesPath = path.join( rootDir, "node_modules", @@ -23,14 +29,22 @@ const terminalCoreBundle = (xtermStyles, terminalCoreStyles) => `${xtermStyles.trim()}\n\n${terminalCoreStyles.trim()}\n`; const terminalBundle = (xtermStyles, terminalCoreStyles, terminalStyles) => `${terminalCoreBundle(xtermStyles, terminalCoreStyles).trim()}\n\n${terminalStyles.trim()}\n`; +const fullStylesBundle = ( + xtermStyles, + terminalCoreStyles, + terminalStyles, + filesystemStyles +) => `${terminalBundle(xtermStyles, terminalCoreStyles, terminalStyles).trim()}\n\n${filesystemStyles.trim()}\n`; await mkdir(distStylesDir, { recursive: true }); -const [xtermStyles, terminalCoreStyles, terminalStyles] = await Promise.all([ - readFile(xtermStylesPath, "utf8"), - readFile(sourceTerminalCoreStylesPath, "utf8"), - readFile(sourceTerminalStylesPath, "utf8"), -]); +const [xtermStyles, terminalCoreStyles, terminalStyles, filesystemStyles] = + await Promise.all([ + readFile(xtermStylesPath, "utf8"), + readFile(sourceTerminalCoreStylesPath, "utf8"), + readFile(sourceTerminalStylesPath, "utf8"), + readFile(sourceFilesystemStylesPath, "utf8"), + ]); await Promise.all([ writeFile( @@ -43,9 +57,19 @@ await Promise.all([ terminalBundle(xtermStyles, terminalCoreStyles, terminalStyles), "utf8" ), + writeFile( + path.join(distStylesDir, "filesystem.css"), + `${filesystemStyles.trim()}\n`, + "utf8" + ), writeFile( path.join(distStylesDir, "styles.css"), - terminalBundle(xtermStyles, terminalCoreStyles, terminalStyles), + fullStylesBundle( + xtermStyles, + terminalCoreStyles, + terminalStyles, + filesystemStyles + ), "utf8" ), ]); diff --git a/src/components/filesystem/CodeEditorPane.tsx b/src/components/filesystem/CodeEditorPane.tsx index d935dd5..71f34b8 100644 --- a/src/components/filesystem/CodeEditorPane.tsx +++ b/src/components/filesystem/CodeEditorPane.tsx @@ -1,259 +1,84 @@ -import { useEffect, useRef, useState } from "react"; -import type * as MonacoEditor from "monaco-editor"; -import { loadMonaco } from "./monaco-loader"; +import React from "react"; + +import type { CSSProperties } from "react"; import type { FileDocument, ResolvedFileWorkspaceTheme } from "./types"; type CodeEditorPaneProps = { document: FileDocument | null; - monacoVsPath?: string; - onChange: (nextValue: string) => void; - onSave?: () => void; theme: ResolvedFileWorkspaceTheme; }; -function ensureTheme( - monaco: typeof MonacoEditor, - theme: ResolvedFileWorkspaceTheme -): string { - const themeName = `hb-filesystem-${theme.id}`; - monaco.editor.defineTheme(themeName, { - base: "vs", - inherit: true, - colors: { - "editor.background": theme.chrome.editorBackground, - "editor.foreground": theme.chrome.text, - "editor.lineHighlightBackground": theme.chrome.rowHover, - "editor.selectionBackground": theme.chrome.rowActive, - "editorCursor.foreground": theme.chrome.accent, - "editorIndentGuide.background1": theme.chrome.border, - "editorLineNumber.foreground": theme.chrome.textMuted, - "editorLineNumber.activeForeground": theme.chrome.text, - }, - rules: [], - }); - return themeName; +function toLineCount(contents: string): number { + if (!contents) { + return 1; + } + return contents.split("\n").length; } -export function CodeEditorPane({ - document, - onChange, - onSave, - theme, -}: CodeEditorPaneProps) { - const changeHandlerRef = useRef(onChange); - const containerRef = useRef(null); - const editorRef = useRef(null); - const modelsRef = useRef>(new Map()); - const monacoRef = useRef(null); - const saveHandlerRef = useRef(onSave); - const syncRef = useRef(false); - const viewStatesRef = useRef< - Map - >(new Map()); - const visibleDocumentPathRef = useRef(null); - const [loadState, setLoadState] = useState<"loading" | "ready" | "error">( - "loading" - ); - const [loadError, setLoadError] = useState(null); - - changeHandlerRef.current = onChange; - saveHandlerRef.current = onSave; - - useEffect(() => { - let active = true; - - async function setupEditor() { - if (!containerRef.current) { - return; - } - - try { - const monaco = await loadMonaco(); - if (!active || !containerRef.current) { - return; - } - - monacoRef.current = monaco; - const themeName = ensureTheme(monaco, theme); - const editor = monaco.editor.create(containerRef.current, { - automaticLayout: true, - fontFamily: theme.editor.fontFamily, - fontSize: theme.editor.fontSize, - lineHeight: Math.round(theme.editor.fontSize * theme.editor.lineHeight), - minimap: { - enabled: false, - }, - padding: { - top: 18, - bottom: 18, - }, - readOnly: true, - renderWhitespace: "selection", - scrollBeyondLastLine: false, - smoothScrolling: true, - tabSize: 2, - theme: themeName, - }); - - editor.onDidChangeModelContent(() => { - if (syncRef.current) { - return; - } - const currentModel = editor.getModel(); - if (!currentModel) { - return; - } - changeHandlerRef.current(currentModel.getValue()); - }); - - editor.onKeyDown((event) => { - if ( - saveHandlerRef.current && - (event.metaKey || event.ctrlKey) && - event.keyCode === monaco.KeyCode.KeyS - ) { - event.preventDefault(); - event.stopPropagation(); - saveHandlerRef.current(); - } - }); - - editorRef.current = editor; - setLoadState("ready"); - setLoadError(null); - } catch (error) { - if (!active) { - return; - } - setLoadError( - error instanceof Error ? error.message : "Failed to load Monaco editor." - ); - setLoadState("error"); - } - } - - void setupEditor(); - - return () => { - active = false; - visibleDocumentPathRef.current = null; - for (const model of modelsRef.current.values()) { - model.dispose(); - } - modelsRef.current.clear(); - viewStatesRef.current.clear(); - editorRef.current?.dispose(); - editorRef.current = null; - monacoRef.current = null; - }; - }, [theme]); - - useEffect(() => { - const editor = editorRef.current; - const monaco = monacoRef.current; - if (!editor || !monaco) { - return; - } - - const themeName = ensureTheme(monaco, theme); - monaco.editor.setTheme(themeName); - editor.updateOptions({ - fontFamily: theme.editor.fontFamily, - fontSize: theme.editor.fontSize, - lineHeight: Math.round(theme.editor.fontSize * theme.editor.lineHeight), - }); - }, [theme]); - - useEffect(() => { - const editor = editorRef.current; - const monaco = monacoRef.current; - if (!editor || !monaco) { - return; - } - - const previousPath = visibleDocumentPathRef.current; - if (previousPath && previousPath !== document?.path) { - viewStatesRef.current.set(previousPath, editor.saveViewState()); - } - - if (!document) { - visibleDocumentPathRef.current = null; - editor.setModel(null); - return; - } - - let model = modelsRef.current.get(document.path); - if (!model) { - model = monaco.editor.createModel( - document.contents, - document.language, - monaco.Uri.parse(`file://${document.path}`) - ); - modelsRef.current.set(document.path, model); - } - - if (model.getValue() !== document.contents) { - syncRef.current = true; - model.setValue(document.contents); - syncRef.current = false; - } - - if (document.language) { - monaco.editor.setModelLanguage(model, document.language); - } - - editor.setModel(model); - editor.updateOptions({ - readOnly: document.readOnly === true, - }); - const savedViewState = viewStatesRef.current.get(document.path); - if (savedViewState) { - editor.restoreViewState(savedViewState); - } - editor.focus(); - visibleDocumentPathRef.current = document.path; - }, [document]); - +export function CodeEditorPane({ document, theme }: CodeEditorPaneProps) { if (!document) { return (
-

Choose a file to start editing

+

Choose a file to preview

- Open a file from the explorer to inspect or edit its contents. + Open a file from the explorer to inspect its contents. This workspace is + currently focused on browsing and read-only previews.

); } - if (loadState === "error") { - return ( -
-
- {loadError} -
-