From 0ed443e24449edc0e43256425cc613a3b00715fc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 18:53:37 +0000 Subject: [PATCH 1/6] fix(editor): fix file loading spinner that never resolves Root cause: Monaco editor had no worker configuration (self.MonacoEnvironment), causing workers to fail silently in Vite/Tauri and the editor to hang during initialization. Changes: - Add MonacoEnvironment.getWorker() config with proper Vite worker URLs using new URL(..., import.meta.url) for all Monaco workers (editor, JSON, CSS, HTML, TypeScript) - Replace static CodeEditor import in MultiBuffer.tsx with lazy() import to prevent pulling all editor dependencies into the initial chunk - Add 15s timeout on Monaco loading to prevent permanent loading spinners - Add retry limit (max 2 attempts) in createEffect to prevent infinite retry loops when Monaco fails to load https://claude.ai/code/session_015pVrWpDDycPtrZNiJEod36 --- src/components/editor/MultiBuffer.tsx | 22 ++++++-- src/components/editor/core/EditorInstance.tsx | 20 ++++++- src/utils/monacoManager.ts | 56 ++++++++++++++++++- 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/components/editor/MultiBuffer.tsx b/src/components/editor/MultiBuffer.tsx index bafb3d2..9bcf8e2 100644 --- a/src/components/editor/MultiBuffer.tsx +++ b/src/components/editor/MultiBuffer.tsx @@ -1,6 +1,5 @@ import { Show, Suspense, createSignal, createMemo, createEffect, onMount, onCleanup, JSX, lazy, For } from "solid-js"; import { useEditor, SplitDirection, OpenFile, EditorGroup } from "@/context/EditorContext"; -import { CodeEditor } from "./CodeEditor"; import { LazyEditor } from "./LazyEditor"; import { TabBar } from "./TabBar"; import { ImageViewer, isImageFile, SVGPreview, isSVGFile } from "../viewers"; @@ -9,6 +8,11 @@ import { Card, Text } from "@/components/ui"; import { safeGetItem, safeSetItem } from "@/utils/safeStorage"; import "@/styles/animations.css"; +// Lazy load CodeEditor - avoids pulling all editor dependencies into the initial chunk +const CodeEditorLazy = lazy(() => + import("./CodeEditor").then((m) => ({ default: m.CodeEditor })), +); + // Lazy load DiffEditor for better performance - only loaded when needed const DiffEditorLazy = lazy(() => import("./DiffEditor")); @@ -278,7 +282,9 @@ function FileViewer(props: FileViewerProps) { when={isNonSvgImage()} fallback={
- + Loading editor...
}> + + } > @@ -466,13 +472,15 @@ export function DiffView(props: DiffViewProps) { > {props.leftFile.name} (Original) - + Loading...}> + + )} second={() => (
-
{props.rightFile.name} (Modified)
- + Loading...
}> + + )} /> diff --git a/src/components/editor/core/EditorInstance.tsx b/src/components/editor/core/EditorInstance.tsx index fbfa798..eaace28 100644 --- a/src/components/editor/core/EditorInstance.tsx +++ b/src/components/editor/core/EditorInstance.tsx @@ -85,6 +85,9 @@ export function createEditorInstance(props: { let currentFileId: string | null = null; let currentFilePath: string | null = null; let editorInitialized = false; + let monacoLoadAttempts = 0; + const MAX_MONACO_LOAD_ATTEMPTS = 2; + const MONACO_LOAD_TIMEOUT_MS = 15000; const [isLoading, setIsLoading] = createSignal(true); const [currentEditor, setCurrentEditor] = @@ -111,7 +114,16 @@ export function createEditorInstance(props: { if (!monacoManager.isLoaded()) { try { - const monaco = await monacoManager.ensureLoaded(); + monacoLoadAttempts++; + // Add timeout to prevent permanent loading spinner + const loadPromise = monacoManager.ensureLoaded(); + const timeoutPromise = new Promise((_, reject) => + setTimeout( + () => reject(new Error("Monaco loading timed out")), + MONACO_LOAD_TIMEOUT_MS, + ), + ); + const monaco = await Promise.race([loadPromise, timeoutPromise]); monacoInstance = monaco; setCurrentMonaco(monaco); @@ -145,6 +157,12 @@ export function createEditorInstance(props: { if (!containerRef || isLoading()) return; if (!monacoManager.isLoaded() && file) { + // Prevent infinite retry loops if Monaco repeatedly fails to load + if (monacoLoadAttempts >= MAX_MONACO_LOAD_ATTEMPTS) { + console.error("[CodeEditor] Monaco failed to load after max attempts, giving up"); + return; + } + monacoLoadAttempts++; setIsLoading(true); monacoManager .ensureLoaded() diff --git a/src/utils/monacoManager.ts b/src/utils/monacoManager.ts index 82d13af..8606ec5 100644 --- a/src/utils/monacoManager.ts +++ b/src/utils/monacoManager.ts @@ -244,8 +244,60 @@ class MonacoManager { * Load Monaco dynamically */ private async loadMonaco(): Promise { - // Use direct ES module import instead of @monaco-editor/loader - // This avoids CDN loading and lets Vite handle bundling + // Configure Monaco worker URLs before importing. + // In Vite, workers must be referenced via `new URL(..., import.meta.url)` + // so that Vite can resolve and bundle them correctly. + // Without this, Monaco fails to create web workers for tokenization + // and the editor may hang or never fully initialize. + self.MonacoEnvironment = { + getWorker(_workerId: string, label: string): Worker { + if (label === "json") { + return new Worker( + new URL( + "monaco-editor/esm/vs/language/json/json.worker.js", + import.meta.url, + ), + { type: "module" }, + ); + } + if (label === "css" || label === "scss" || label === "less") { + return new Worker( + new URL( + "monaco-editor/esm/vs/language/css/css.worker.js", + import.meta.url, + ), + { type: "module" }, + ); + } + if (label === "html" || label === "handlebars" || label === "razor") { + return new Worker( + new URL( + "monaco-editor/esm/vs/language/html/html.worker.js", + import.meta.url, + ), + { type: "module" }, + ); + } + if (label === "typescript" || label === "javascript") { + return new Worker( + new URL( + "monaco-editor/esm/vs/language/typescript/ts.worker.js", + import.meta.url, + ), + { type: "module" }, + ); + } + // Default editor worker (handles tokenization, diff, etc.) + return new Worker( + new URL( + "monaco-editor/esm/vs/editor/editor.worker.js", + import.meta.url, + ), + { type: "module" }, + ); + }, + }; + const monaco = await import("monaco-editor"); return monaco; } From 6abde76c602c5a9279e847712c05581e2725ce3f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 19:38:49 +0000 Subject: [PATCH 2/6] fix(editor): pre-bundle Monaco and add error boundary for editor loading The infinite loading spinner was caused by Monaco not being in Vite's optimizeDeps.include. Without pre-bundling, Vite serves 500+ individual ESM files on first dynamic import, triggering an on-the-fly re-optimization that blocks the import indefinitely. Changes: - Add "monaco-editor" to optimizeDeps.include in vite.config.ts so Vite pre-bundles it at dev server start instead of on first request - Add ErrorBoundary around LazyEditor's Suspense to catch and display module load failures with a retry button, instead of an infinite spinner https://claude.ai/code/session_015pVrWpDDycPtrZNiJEod36 --- src/components/editor/LazyEditor.tsx | 43 +++++++++++++++++++++++++--- vite.config.ts | 3 ++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/components/editor/LazyEditor.tsx b/src/components/editor/LazyEditor.tsx index 5d0685b..f452dbc 100644 --- a/src/components/editor/LazyEditor.tsx +++ b/src/components/editor/LazyEditor.tsx @@ -8,7 +8,7 @@ * */ -import { Show, createSignal, createEffect, onCleanup, lazy, Suspense } from "solid-js"; +import { Show, createSignal, createEffect, onCleanup, lazy, Suspense, ErrorBoundary } from "solid-js"; import type { OpenFile } from "@/context/EditorContext"; import { EditorSkeleton } from "./EditorSkeleton"; @@ -51,9 +51,44 @@ export function LazyEditor(props: LazyEditorProps) { data-active={props.isActive} > - }> - - + ( +
+
+ Failed to load editor for {props.file.name} +
+
+ {err?.message || "Unknown error"} +
+ +
+ )}> + }> + + +
); diff --git a/vite.config.ts b/vite.config.ts index 2cdc47f..c2c15ef 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -179,6 +179,9 @@ export default defineConfig(async (): Promise => ({ "@tauri-apps/plugin-os", "marked", "diff", + // Monaco must be pre-bundled; without this, Vite serves 500+ individual + // ESM files on first dynamic import, causing the editor to hang indefinitely. + "monaco-editor", ], esbuildOptions: { target: "es2021", From 6665753d5b1c4bb68b530a1e0c6370028bcdffc9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 19:48:57 +0000 Subject: [PATCH 3/6] fix(editor): convert containerRef to signal to fix reactivity deadlock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROOT CAUSE: containerRef was a plain `let` variable, not a SolidJS signal. The createEffect that creates the Monaco editor checks: if (!containerRef || isLoading()) return; Since containerRef is not reactive, setting it via the ref callback does NOT trigger the effect to re-run. When Monaco is already cached (isLoading flips to false before the ref callback fires), the effect runs, sees containerRef as undefined, returns early, and NEVER re-runs — causing the infinite "Loading editor..." spinner. Fix: Convert containerRef to createSignal() so the createEffect properly tracks it as a dependency and re-runs when the ref callback sets the container element. https://claude.ai/code/session_015pVrWpDDycPtrZNiJEod36 --- src/components/editor/core/EditorInstance.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/editor/core/EditorInstance.tsx b/src/components/editor/core/EditorInstance.tsx index eaace28..2614ef8 100644 --- a/src/components/editor/core/EditorInstance.tsx +++ b/src/components/editor/core/EditorInstance.tsx @@ -56,7 +56,7 @@ export function getMonacoInstance(): typeof Monaco | null { export interface CreateEditorInstanceResult { editor: Accessor; monaco: Accessor; - containerRef: HTMLDivElement | undefined; + containerRef: Accessor; setContainerRef: (el: HTMLDivElement) => void; isLoading: Accessor; activeFile: Accessor; @@ -79,7 +79,7 @@ export function createEditorInstance(props: { getEffectiveEditorSettings, } = useSettings(); - let containerRef: HTMLDivElement | undefined; + const [containerRef, setContainerRef] = createSignal(undefined); let editorRef: Monaco.editor.IStandaloneCodeEditor | null = null; let isDisposed = false; let currentFileId: string | null = null; @@ -154,7 +154,8 @@ export function createEditorInstance(props: { const fileId = file?.id || null; const filePath = file?.path || null; - if (!containerRef || isLoading()) return; + const container = containerRef(); + if (!container || isLoading()) return; if (!monacoManager.isLoaded() && file) { // Prevent infinite retry loops if Monaco repeatedly fails to load @@ -403,7 +404,7 @@ export function createEditorInstance(props: { ); } else { editorRef = monacoInstance!.editor.create( - containerRef, + container, editorOptions, ); editorInitialized = true; @@ -529,8 +530,9 @@ export function createEditorInstance(props: { setCurrentEditor(null); } - if (containerRef) { - containerRef.innerHTML = ""; + const el = containerRef(); + if (el) { + el.innerHTML = ""; } }); @@ -539,7 +541,7 @@ export function createEditorInstance(props: { monaco: currentMonaco, containerRef, setContainerRef: (el: HTMLDivElement) => { - containerRef = el; + setContainerRef(el); }, isLoading, activeFile, From 5af68d75c0b8f6726de241cce53a28eefe077f69 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 20:10:22 +0000 Subject: [PATCH 4/6] fix(editor): fix zero-height container on WebKitGTK and add worker fallback The editor container had `height: "auto"` which overrides `flex: 1` on WebKitGTK (Tauri/Linux), causing Monaco to render into a 0-height div. Changed to `height: "100%"` so the flex layout properly sizes the container. Also added try/catch fallback for Monaco web workers: module workers (`{ type: "module" }`) may not be supported on WebKitGTK, so we now fall back to classic workers via blob URLs when module workers fail. https://claude.ai/code/session_015pVrWpDDycPtrZNiJEod36 --- src/components/editor/EditorPanel.tsx | 2 +- src/utils/monacoManager.ts | 67 +++++++++++++-------------- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/components/editor/EditorPanel.tsx b/src/components/editor/EditorPanel.tsx index 4c67008..e65986b 100644 --- a/src/components/editor/EditorPanel.tsx +++ b/src/components/editor/EditorPanel.tsx @@ -93,7 +93,7 @@ export function EditorPanel() { visibility: showEditor() ? "visible" : "hidden", position: showEditor() ? "relative" : "absolute", width: "100%", - height: showEditor() ? "auto" : "0", + height: showEditor() ? "100%" : "0", "pointer-events": showEditor() ? "auto" : "none", }} > diff --git a/src/utils/monacoManager.ts b/src/utils/monacoManager.ts index 8606ec5..7ad60bf 100644 --- a/src/utils/monacoManager.ts +++ b/src/utils/monacoManager.ts @@ -249,52 +249,49 @@ class MonacoManager { // so that Vite can resolve and bundle them correctly. // Without this, Monaco fails to create web workers for tokenization // and the editor may hang or never fully initialize. + // + // WebKitGTK (used by Tauri on Linux) may not support module workers + // (`{ type: "module" }`), so we try module first and fall back to classic. + const workerUrls: Record = { + json: new URL("monaco-editor/esm/vs/language/json/json.worker.js", import.meta.url), + css: new URL("monaco-editor/esm/vs/language/css/css.worker.js", import.meta.url), + html: new URL("monaco-editor/esm/vs/language/html/html.worker.js", import.meta.url), + typescript: new URL("monaco-editor/esm/vs/language/typescript/ts.worker.js", import.meta.url), + editor: new URL("monaco-editor/esm/vs/editor/editor.worker.js", import.meta.url), + }; + + function createWorkerWithFallback(url: URL): Worker { + try { + // Try module worker first (works in Chromium, Firefox) + return new Worker(url, { type: "module" }); + } catch { + // Fallback for WebKitGTK/Safari where module workers may not be supported. + // Use a classic worker that imports the module via importScripts or blob. + console.warn("[Monaco] Module workers not supported, falling back to classic worker"); + const blob = new Blob( + [`importScripts(${JSON.stringify(url.toString())});`], + { type: "application/javascript" }, + ); + return new Worker(URL.createObjectURL(blob)); + } + } + self.MonacoEnvironment = { getWorker(_workerId: string, label: string): Worker { if (label === "json") { - return new Worker( - new URL( - "monaco-editor/esm/vs/language/json/json.worker.js", - import.meta.url, - ), - { type: "module" }, - ); + return createWorkerWithFallback(workerUrls.json); } if (label === "css" || label === "scss" || label === "less") { - return new Worker( - new URL( - "monaco-editor/esm/vs/language/css/css.worker.js", - import.meta.url, - ), - { type: "module" }, - ); + return createWorkerWithFallback(workerUrls.css); } if (label === "html" || label === "handlebars" || label === "razor") { - return new Worker( - new URL( - "monaco-editor/esm/vs/language/html/html.worker.js", - import.meta.url, - ), - { type: "module" }, - ); + return createWorkerWithFallback(workerUrls.html); } if (label === "typescript" || label === "javascript") { - return new Worker( - new URL( - "monaco-editor/esm/vs/language/typescript/ts.worker.js", - import.meta.url, - ), - { type: "module" }, - ); + return createWorkerWithFallback(workerUrls.typescript); } // Default editor worker (handles tokenization, diff, etc.) - return new Worker( - new URL( - "monaco-editor/esm/vs/editor/editor.worker.js", - import.meta.url, - ), - { type: "module" }, - ); + return createWorkerWithFallback(workerUrls.editor); }, }; From 9923c6758bb84aef4fe25853caf95740fd125757 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 20:33:22 +0000 Subject: [PATCH 5/6] fix(editor): eliminate silent loading hangs on WebKitGTK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three interacting bugs caused "Loading editor..." to show forever: 1. LazyEditor used lazy()+Suspense for CodeEditor, but on WebKitGTK the dynamic chunk import can silently hang — Suspense has no timeout, so the fallback skeleton shows forever. Fixed by using a direct import. 2. EditorInstance's createEffect retry called monacoManager.ensureLoaded() without a timeout. After the initial 15s timeout in onMount, the retry got back the SAME dead promise (MonacoManager was still in "loading" state), hanging forever. Fixed by adding Promise.race timeout to retry. 3. MonacoManager had no way to reset from a timed-out load — it stayed in "loading" state with a dead promise. Added resetLoadState() method, called on timeout/error to allow fresh retry attempts. https://claude.ai/code/session_015pVrWpDDycPtrZNiJEod36 --- src/components/editor/LazyEditor.tsx | 51 +++---------------- src/components/editor/core/EditorInstance.tsx | 16 +++++- src/utils/monacoManager.ts | 12 +++++ 3 files changed, 33 insertions(+), 46 deletions(-) diff --git a/src/components/editor/LazyEditor.tsx b/src/components/editor/LazyEditor.tsx index f452dbc..eb31210 100644 --- a/src/components/editor/LazyEditor.tsx +++ b/src/components/editor/LazyEditor.tsx @@ -4,17 +4,17 @@ * Background tabs have their Monaco model preserved but the editor DOM * is unmounted to reduce memory pressure and DOM node count. * + * Uses a direct import of CodeEditor instead of SolidJS lazy()+Suspense + * because the lazy chunk import can silently hang on WebKitGTK (Linux/Tauri), + * leaving the Suspense fallback showing forever with no timeout or error. + * * Usage in EditorGroupPanel (MultiBuffer.tsx): * */ -import { Show, createSignal, createEffect, onCleanup, lazy, Suspense, ErrorBoundary } from "solid-js"; +import { Show, createSignal, createEffect, onCleanup } from "solid-js"; import type { OpenFile } from "@/context/EditorContext"; -import { EditorSkeleton } from "./EditorSkeleton"; - -const CodeEditorLazy = lazy(() => - import("./CodeEditor").then((m) => ({ default: m.CodeEditor })), -); +import { CodeEditor } from "./CodeEditor"; export interface LazyEditorProps { file: OpenFile; @@ -51,44 +51,7 @@ export function LazyEditor(props: LazyEditorProps) { data-active={props.isActive} > - ( -
-
- Failed to load editor for {props.file.name} -
-
- {err?.message || "Unknown error"} -
- -
- )}> - }> - - -
+
); diff --git a/src/components/editor/core/EditorInstance.tsx b/src/components/editor/core/EditorInstance.tsx index 2614ef8..fcf3ce4 100644 --- a/src/components/editor/core/EditorInstance.tsx +++ b/src/components/editor/core/EditorInstance.tsx @@ -135,6 +135,9 @@ export function createEditorInstance(props: { } } catch (error) { console.error("Failed to load Monaco editor:", error); + // Reset the MonacoManager state so retries start fresh instead of + // returning the same dead promise that timed out. + monacoManager.resetLoadState(); setIsLoading(false); return; } @@ -165,8 +168,16 @@ export function createEditorInstance(props: { } monacoLoadAttempts++; setIsLoading(true); - monacoManager - .ensureLoaded() + // Use timeout on retry too — without this, a dead promise from a + // previous timed-out attempt would hang forever. + const retryLoad = monacoManager.ensureLoaded(); + const retryTimeout = new Promise((_, reject) => + setTimeout( + () => reject(new Error("Monaco retry timed out")), + MONACO_LOAD_TIMEOUT_MS, + ), + ); + Promise.race([retryLoad, retryTimeout]) .then((monaco) => { monacoInstance = monaco; setCurrentMonaco(monaco); @@ -174,6 +185,7 @@ export function createEditorInstance(props: { }) .catch((err) => { console.error("Failed to load Monaco editor:", err); + monacoManager.resetLoadState(); setIsLoading(false); }); return; diff --git a/src/utils/monacoManager.ts b/src/utils/monacoManager.ts index 7ad60bf..9004294 100644 --- a/src/utils/monacoManager.ts +++ b/src/utils/monacoManager.ts @@ -187,6 +187,18 @@ class MonacoManager { return this.loadState === "loaded" && this.monaco !== null; } + /** + * Reset load state so a fresh load attempt can be made. + * Called when a previous load timed out or errored, leaving the manager + * stuck in "loading" state with a dead promise that will never resolve. + */ + resetLoadState(): void { + if (this.loadState !== "loaded") { + this.loadState = "idle"; + this.loadPromise = null; + } + } + /** * Get the Monaco instance (throws if not loaded) */ From c3d4bd041e7e525988694b0b1c0015838e424ccf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 20:59:20 +0000 Subject: [PATCH 6/6] fix(editor): add diagnostic logging and preload Monaco at startup Adds console.warn diagnostics at every stage of the editor loading chain to identify the exact stuck point on WebKitGTK: - EditorPanel: logs isOpening, hasOpenFiles, showEditor state changes - openFile: logs IPC call start/completion and isOpening transitions - LazyEditor: logs file activation and wasEverActive state - EditorInstance: logs onMount, Monaco load status, createEffect state - MonacoManager: logs ensureLoaded state machine, import() resolution Also preloads Monaco at EditorProvider startup (Tier 1 mount) so the editor module is already loaded by the time a user opens their first file. This eliminates the 15+ second wait on first file open. https://claude.ai/code/session_015pVrWpDDycPtrZNiJEod36 --- src/components/editor/EditorPanel.tsx | 13 +++++++++---- src/components/editor/LazyEditor.tsx | 3 +++ src/components/editor/core/EditorInstance.tsx | 9 ++++++++- src/context/editor/EditorProvider.tsx | 15 ++++++++++++++- src/context/editor/fileOperations.ts | 9 +++++++-- src/utils/monacoManager.ts | 13 ++++++++++--- 6 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/components/editor/EditorPanel.tsx b/src/components/editor/EditorPanel.tsx index e65986b..b3320ef 100644 --- a/src/components/editor/EditorPanel.tsx +++ b/src/components/editor/EditorPanel.tsx @@ -1,4 +1,4 @@ -import { Show, createMemo, type JSX } from "solid-js"; +import { Show, createMemo, createEffect, type JSX } from "solid-js"; import { MultiBuffer } from "./MultiBuffer"; import { useEditor } from "@/context/EditorContext"; import { EditorSkeleton } from "./EditorSkeleton"; @@ -22,14 +22,19 @@ import { tokens } from "@/design-system/tokens"; */ export function EditorPanel() { const { state } = useEditor(); - + const hasOpenFiles = createMemo(() => state.openFiles.length > 0); const showEditor = createMemo(() => !state.isOpening && hasOpenFiles()); + // Diagnostic logging to trace editor loading flow + createEffect(() => { + console.warn("[EditorPanel] isOpening:", state.isOpening, "| hasOpenFiles:", hasOpenFiles(), "| showEditor:", showEditor(), "| openFiles count:", state.openFiles.length); + }); + return ( -
(); export function LazyEditor(props: LazyEditorProps) { const [wasEverActive, setWasEverActive] = createSignal(props.isActive); + console.warn("[LazyEditor] Created for file:", props.file.name, "| isActive:", props.isActive, "| wasEverActive:", wasEverActive()); + createEffect(() => { if (props.isActive) { setWasEverActive(true); mountedModels.add(props.file.id); + console.warn("[LazyEditor] File became active:", props.file.name, "| wasEverActive set to true"); } }); diff --git a/src/components/editor/core/EditorInstance.tsx b/src/components/editor/core/EditorInstance.tsx index fcf3ce4..70bb7f3 100644 --- a/src/components/editor/core/EditorInstance.tsx +++ b/src/components/editor/core/EditorInstance.tsx @@ -107,7 +107,9 @@ export function createEditorInstance(props: { onMount(async () => { const file = activeFile(); + console.warn("[EditorInstance] onMount fired | file:", file?.name, "| monacoLoaded:", monacoManager.isLoaded(), "| loadState:", monacoManager.getLoadState()); if (!file) { + console.warn("[EditorInstance] No file, setting isLoading=false"); setIsLoading(false); return; } @@ -115,6 +117,7 @@ export function createEditorInstance(props: { if (!monacoManager.isLoaded()) { try { monacoLoadAttempts++; + console.warn("[EditorInstance] Starting Monaco load, attempt:", monacoLoadAttempts); // Add timeout to prevent permanent loading spinner const loadPromise = monacoManager.ensureLoaded(); const timeoutPromise = new Promise((_, reject) => @@ -124,6 +127,7 @@ export function createEditorInstance(props: { ), ); const monaco = await Promise.race([loadPromise, timeoutPromise]); + console.warn("[EditorInstance] Monaco loaded successfully"); monacoInstance = monaco; setCurrentMonaco(monaco); @@ -134,7 +138,7 @@ export function createEditorInstance(props: { }); } } catch (error) { - console.error("Failed to load Monaco editor:", error); + console.error("[EditorInstance] Failed to load Monaco:", error); // Reset the MonacoManager state so retries start fresh instead of // returning the same dead promise that timed out. monacoManager.resetLoadState(); @@ -142,10 +146,12 @@ export function createEditorInstance(props: { return; } } else { + console.warn("[EditorInstance] Monaco already loaded"); monacoInstance = monacoManager.getMonaco(); setCurrentMonaco(monacoInstance); } + console.warn("[EditorInstance] Setting isLoading=false"); setIsLoading(false); }); @@ -158,6 +164,7 @@ export function createEditorInstance(props: { const filePath = file?.path || null; const container = containerRef(); + console.warn("[EditorInstance] createEffect | file:", file?.name, "| container:", !!container, "| isLoading:", isLoading(), "| monacoLoaded:", monacoManager.isLoaded()); if (!container || isLoading()) return; if (!monacoManager.isLoaded() && file) { diff --git a/src/context/editor/EditorProvider.tsx b/src/context/editor/EditorProvider.tsx index 30d4b5d..06a3e5f 100644 --- a/src/context/editor/EditorProvider.tsx +++ b/src/context/editor/EditorProvider.tsx @@ -1,6 +1,7 @@ -import { createContext, useContext, ParentProps, createMemo, batch } from "solid-js"; +import { createContext, useContext, ParentProps, createMemo, batch, onMount } from "solid-js"; import { createStore, produce } from "solid-js/store"; import { loadGridState } from "../../utils/gridSerializer"; +import { MonacoManager } from "../../utils/monacoManager"; import type { OpenFile, EditorGroup, EditorSplit, SplitDirection } from "../../types"; import type { EditorState, @@ -393,6 +394,18 @@ export function EditorProvider(props: ParentProps) { saveFile: fileOps.saveFile, }); + // Preload Monaco at startup so it's ready when the first file is opened. + // This runs after the EditorProvider mounts (Tier 1), giving Monaco time + // to load in the background while the user navigates the file tree. + onMount(() => { + console.warn("[EditorProvider] Preloading Monaco editor in background..."); + MonacoManager.getInstance().ensureLoaded().then(() => { + console.warn("[EditorProvider] Monaco preloaded successfully"); + }).catch((err) => { + console.warn("[EditorProvider] Monaco preload failed (will retry on file open):", err); + }); + }); + return ( { const perfStart = performance.now(); const targetGroupId = groupId || state.activeGroupId; - + console.warn("[openFile] Called for path:", path, "| targetGroupId:", targetGroupId); + const existing = state.openFiles.find((f) => f.path === path); if (existing) { + console.warn("[openFile] File already open, switching to existing:", existing.id); batch(() => { setState("activeFileId", existing.id); setState("activeGroupId", targetGroupId); @@ -36,10 +38,12 @@ export function createFileOperations( } setState("isOpening", true); + console.warn("[openFile] isOpening set to TRUE"); try { const readStart = performance.now(); + console.warn("[openFile] Calling fs_read_file IPC..."); const content = await invoke("fs_read_file", { path }); - console.debug(`[EditorContext] fs_read_file: ${(performance.now() - readStart).toFixed(1)}ms (${(content.length / 1024).toFixed(1)}KB)`); + console.warn(`[openFile] fs_read_file completed: ${(performance.now() - readStart).toFixed(1)}ms (${(content.length / 1024).toFixed(1)}KB)`); const name = path.split(/[/\\]/).pop() || path; const id = `file-${generateId()}`; @@ -86,6 +90,7 @@ export function createFileOperations( ); } finally { setState("isOpening", false); + console.warn("[openFile] isOpening set to FALSE (finally block)"); } }; diff --git a/src/utils/monacoManager.ts b/src/utils/monacoManager.ts index 9004294..8d90c71 100644 --- a/src/utils/monacoManager.ts +++ b/src/utils/monacoManager.ts @@ -222,31 +222,36 @@ class MonacoManager { async ensureLoaded(): Promise { // Already loaded if (this.loadState === "loaded" && this.monaco) { + console.warn("[MonacoManager] ensureLoaded: already loaded"); return this.monaco; } // Loading in progress if (this.loadState === "loading" && this.loadPromise) { + console.warn("[MonacoManager] ensureLoaded: load already in progress, waiting..."); return this.loadPromise; } // Start loading + console.warn("[MonacoManager] ensureLoaded: starting fresh load, current state:", this.loadState); this.loadState = "loading"; this.config.onLoadStart(); this.loadPromise = this.loadMonaco(); - + try { this.monaco = await this.loadPromise; this.loadState = "loaded"; this.config.onLoadComplete(); - + console.warn("[MonacoManager] ensureLoaded: Monaco loaded successfully"); + // Register theme and providers this.registerTheme(); - + return this.monaco; } catch (error) { this.loadState = "error"; + console.error("[MonacoManager] ensureLoaded: load failed:", error); this.config.onLoadError(error instanceof Error ? error : new Error(String(error))); throw error; } @@ -307,7 +312,9 @@ class MonacoManager { }, }; + console.warn("[MonacoManager] loadMonaco: starting import('monaco-editor')..."); const monaco = await import("monaco-editor"); + console.warn("[MonacoManager] loadMonaco: import('monaco-editor') resolved successfully"); return monaco; }