diff --git a/examples/TscircuitWorkspaceWithRunframe.fixture.tsx b/examples/TscircuitWorkspaceWithRunframe.fixture.tsx index 92f210a..b6ea7be 100644 --- a/examples/TscircuitWorkspaceWithRunframe.fixture.tsx +++ b/examples/TscircuitWorkspaceWithRunframe.fixture.tsx @@ -1,30 +1,104 @@ import { useMemo, useState } from "react" import "../src/styles.css" +import type { EditorFile } from "../src" +import { resolveTscircuitEntrypoint } from "../fixtures-support/resolveTscircuitEntrypoint" import { SuspenseRunFrame } from "../src/components/SuspenseRunFrame" -import { - WorkspaceCodeEditor, - type EditorFile, -} from "../src/components/WorkspaceCodeEditor" +import { WorkspaceCodeEditor } from "../src/components/WorkspaceCodeEditor" import { useWorkspaceFiles } from "../src/hooks/useWorkspaceFiles" const initialFiles: EditorFile[] = [ { path: "index.tsx", - content: `export default () => ( - - - + content: `import manualEdits from "./manual-edits.json" +import { MPL3115A2R1 } from "./MPL3115A2R1" +import { sel } from "tscircuit" + +export default () => ( + + + + + + + -)`, +) +`, + }, + { + path: "MPL3115A2R1.tsx", + content: `import type { ChipProps } from "@tscircuit/props" + +const pinLabels = { + pin1: ["VDD"], + pin2: ["CAP"], + pin3: ["GND"], + pin4: ["VDDIO"], + pin5: ["INT2"], + pin6: ["INT1"], + pin7: ["SDA"], + pin8: ["SCL"], +} as const + +export const MPL3115A2R1 = (props: ChipProps) => { + return ( + + + + + + + + + + + } + /> + ) +} +`, + }, + { + path: "manual-edits.json", + content: `{ + "pcb_placements": [], + "schematic_placements": [], + "edit_events": [] +} +`, }, ] @@ -36,7 +110,6 @@ export default function TscircuitWorkspaceWithRunframeFixture() { const [lastRenderState, setLastRenderState] = useState< "idle" | "running" | "finished" >("idle") - const fsMap = useMemo( () => workspace.files.reduce( @@ -48,6 +121,12 @@ export default function TscircuitWorkspaceWithRunframeFixture() { ), [workspace.files], ) + const mainComponentPath = useMemo( + () => + resolveTscircuitEntrypoint(workspace.files, workspace.currentFile) ?? + "index.tsx", + [workspace.files, workspace.currentFile], + ) return (
@@ -59,7 +138,7 @@ export default function TscircuitWorkspaceWithRunframeFixture() { { + return ( + /export default\s+\w+/.test(code) || + /export default\s+function\s*(\w*)\s*\(/.test(code) || + /export default\s*\(\s*\)\s*=>/.test(code) || + /export default\s*\(.*?\)\s*=>/.test(code) || + /export\s*\{\s*\w+\s+as\s+default\s*\}/.test(code) + ) +} + +const findConfiguredEntrypoint = ( + files: EditorFile[], + field: "mainEntrypoint" | "previewComponentPath", +) => { + const configFile = files.find((file) => file.path === "tscircuit.config.json") + if (!configFile) return null + + try { + const config = JSON.parse(configFile.content) as Record + const configuredPath = config[field] + if (typeof configuredPath !== "string") return null + + const normalizedPath = configuredPath.startsWith("./") + ? configuredPath.slice(2) + : configuredPath + + return files.find((file) => file.path === normalizedPath) ?? null + } catch { + return null + } +} + +export const resolveTscircuitEntrypoint = ( + files: EditorFile[], + currentFile: string | null, +) => { + const currentFileData = currentFile + ? files.find((file) => file.path === currentFile) + : null + + if ( + currentFileData && + /\.(ts|tsx)$/.test(currentFileData.path) && + hasDefaultEntrypointExport(currentFileData.content) + ) { + return currentFileData.path + } + + return ( + findConfiguredEntrypoint(files, "mainEntrypoint")?.path ?? + findConfiguredEntrypoint(files, "previewComponentPath")?.path ?? + files.find((file) => file.path === "index.tsx" || file.path === "index.ts") + ?.path ?? + files.find((file) => file.path.endsWith(".circuit.tsx"))?.path ?? + files.find((file) => file.path === "main.tsx" || file.path === "main.ts") + ?.path ?? + files.find((file) => file.path.endsWith(".tsx"))?.path ?? + null + ) +} diff --git a/src/components/WorkspaceCodeEditor.tsx b/src/components/WorkspaceCodeEditor.tsx index 8812d77..c9be3e8 100644 --- a/src/components/WorkspaceCodeEditor.tsx +++ b/src/components/WorkspaceCodeEditor.tsx @@ -1,5 +1,5 @@ import Editor, { type OnChange, type OnMount } from "@monaco-editor/react" -import { useEffect, useMemo, useRef, useState } from "react" +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react" import * as monaco from "monaco-editor" import { defaultCodeEditorOptions, @@ -101,7 +101,7 @@ export function WorkspaceCodeEditor({ readOnly = false, isSaving = false, isStreaming = false, - isPriorityFileFetched = false, + isPriorityFileFetched, showSidebar = true, className, height = "100%", @@ -109,6 +109,7 @@ export function WorkspaceCodeEditor({ }: WorkspaceCodeEditorProps) { const isReady = useMonacoReady() const [editorReady, setEditorReady] = useState(false) + const [workspaceModelsReady, setWorkspaceModelsReady] = useState(false) const editorRef = useRef(null) const managerRef = useRef(null) @@ -136,6 +137,14 @@ export function WorkspaceCodeEditor({ const currentContentRef = useRef(currentContent) currentContentRef.current = currentContent const currentFileIsBinary = currentFileData?.isBinary === true + const isPriorityFilePending = isPriorityFileFetched === false + const workspaceFiles = useMemo( + () => + files + .filter((file) => !file.isBinary) + .map((file) => ({ path: file.path, content: file.content })), + [files], + ) const editorOptions = useMemo( @@ -155,14 +164,28 @@ export function WorkspaceCodeEditor({ // Keep a live model for every (text) file so the TypeScript language service // can resolve cross-file imports and surface diagnostics project-wide. + useLayoutEffect(() => { + if (!isReady) { + setWorkspaceModelsReady(false) + return + } + + managerRef.current?.syncFiles(workspaceFiles) + setWorkspaceModelsReady(true) + }, [isReady, workspaceFiles]) + + // Monaco's TS worker can occasionally compute diagnostics before it has seen + // the full workspace graph on the first load. Re-sync once after the editor + // mounts to force a fresh pass without requiring a manual page refresh. useEffect(() => { - if (!isReady) return - managerRef.current?.syncFiles( - files - .filter((file) => !file.isBinary) - .map((file) => ({ path: file.path, content: file.content })), - ) - }, [isReady, files]) + if (!isReady || !editorReady) return + + const timeoutId = window.setTimeout(() => { + managerRef.current?.syncFiles(workspaceFiles) + }, 0) + + return () => window.clearTimeout(timeoutId) + }, [isReady, editorReady, workspaceFiles]) // Route cross-file "go to definition" through onFileSelect so navigation into // another workspace file switches the active file instead of failing silently. @@ -348,7 +371,7 @@ export function WorkspaceCodeEditor({ ) : currentFileIsBinary ? ( - ) : !isReady || isPriorityFileFetched ? ( + ) : !isReady || !workspaceModelsReady || isPriorityFilePending ? ( Loading editor… ) : currentFile ? (