Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions src/components/editor/EditorPanel.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 (
<div
<div
class="editor-panel flex-1 flex flex-col min-h-0 overflow-hidden"
style={{
style={{
position: "relative",
background: "var(--vscode-editor-background, #141415)",
}}
Expand Down Expand Up @@ -93,7 +98,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",
}}
>
Expand Down
19 changes: 10 additions & 9 deletions src/components/editor/LazyEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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):
* <LazyEditor file={file} isActive={isActive} groupId={groupId} />
*/

import { Show, createSignal, createEffect, onCleanup, lazy, Suspense } 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;
Expand All @@ -27,10 +27,13 @@ const mountedModels = new Set<string>();
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");
}
});

Expand All @@ -51,9 +54,7 @@ export function LazyEditor(props: LazyEditorProps) {
data-active={props.isActive}
>
<Show when={wasEverActive()}>
<Suspense fallback={<EditorSkeleton />}>
<CodeEditorLazy file={props.file} groupId={props.groupId} />
</Suspense>
<CodeEditor file={props.file} groupId={props.groupId} />
</Show>
</div>
);
Expand Down
22 changes: 16 additions & 6 deletions src/components/editor/MultiBuffer.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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"));

Expand Down Expand Up @@ -278,7 +282,9 @@ function FileViewer(props: FileViewerProps) {
when={isNonSvgImage()}
fallback={
<div class="flex-1 flex flex-col min-h-0 overflow-hidden">
<CodeEditor file={props.file} groupId={props.groupId} />
<Suspense fallback={<div style={{ flex: "1", display: "flex", "align-items": "center", "justify-content": "center" }}>Loading editor...</div>}>
<CodeEditorLazy file={props.file} groupId={props.groupId} />
</Suspense>
</div>
}
>
Expand Down Expand Up @@ -466,13 +472,15 @@ export function DiffView(props: DiffViewProps) {
>
<Text variant="muted" size="sm">{props.leftFile.name} (Original)</Text>
</div>
<CodeEditor file={props.leftFile} />
<Suspense fallback={<div style={{ flex: "1" }}>Loading...</div>}>
<CodeEditorLazy file={props.leftFile} />
</Suspense>
</div>
)}
second={() => (
<div style={{ flex: "1", display: "flex", "flex-direction": "column", overflow: "hidden" }}>
<div
style={{
<div
style={{
height: "32px",
display: "flex",
"align-items": "center",
Expand All @@ -484,7 +492,9 @@ export function DiffView(props: DiffViewProps) {
>
<Text variant="muted" size="sm">{props.rightFile.name} (Modified)</Text>
</div>
<CodeEditor file={props.rightFile} />
<Suspense fallback={<div style={{ flex: "1" }}>Loading...</div>}>
<CodeEditorLazy file={props.rightFile} />
</Suspense>
</div>
)}
/>
Expand Down
61 changes: 50 additions & 11 deletions src/components/editor/core/EditorInstance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function getMonacoInstance(): typeof Monaco | null {
export interface CreateEditorInstanceResult {
editor: Accessor<Monaco.editor.IStandaloneCodeEditor | null>;
monaco: Accessor<typeof Monaco | null>;
containerRef: HTMLDivElement | undefined;
containerRef: Accessor<HTMLDivElement | undefined>;
setContainerRef: (el: HTMLDivElement) => void;
isLoading: Accessor<boolean>;
activeFile: Accessor<OpenFile | undefined>;
Expand All @@ -79,12 +79,15 @@ export function createEditorInstance(props: {
getEffectiveEditorSettings,
} = useSettings();

let containerRef: HTMLDivElement | undefined;
const [containerRef, setContainerRef] = createSignal<HTMLDivElement | undefined>(undefined);
let editorRef: Monaco.editor.IStandaloneCodeEditor | null = null;
let isDisposed = false;
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] =
Expand All @@ -104,14 +107,27 @@ 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;
}

if (!monacoManager.isLoaded()) {
try {
const monaco = await monacoManager.ensureLoaded();
monacoLoadAttempts++;
console.warn("[EditorInstance] Starting Monaco load, attempt:", monacoLoadAttempts);
// Add timeout to prevent permanent loading spinner
const loadPromise = monacoManager.ensureLoaded();
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error("Monaco loading timed out")),
MONACO_LOAD_TIMEOUT_MS,
),
);
const monaco = await Promise.race([loadPromise, timeoutPromise]);
console.warn("[EditorInstance] Monaco loaded successfully");
monacoInstance = monaco;
setCurrentMonaco(monaco);

Expand All @@ -122,15 +138,20 @@ 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();
setIsLoading(false);
return;
}
} else {
console.warn("[EditorInstance] Monaco already loaded");
monacoInstance = monacoManager.getMonaco();
setCurrentMonaco(monacoInstance);
}

console.warn("[EditorInstance] Setting isLoading=false");
setIsLoading(false);
});

Expand All @@ -142,19 +163,36 @@ export function createEditorInstance(props: {
const fileId = file?.id || null;
const filePath = file?.path || null;

if (!containerRef || isLoading()) return;
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) {
// 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()
// 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<never>((_, reject) =>
setTimeout(
() => reject(new Error("Monaco retry timed out")),
MONACO_LOAD_TIMEOUT_MS,
),
);
Promise.race([retryLoad, retryTimeout])
.then((monaco) => {
monacoInstance = monaco;
setCurrentMonaco(monaco);
setIsLoading(false);
})
.catch((err) => {
console.error("Failed to load Monaco editor:", err);
monacoManager.resetLoadState();
setIsLoading(false);
});
return;
Expand Down Expand Up @@ -385,7 +423,7 @@ export function createEditorInstance(props: {
);
} else {
editorRef = monacoInstance!.editor.create(
containerRef,
container,
editorOptions,
);
editorInitialized = true;
Expand Down Expand Up @@ -511,8 +549,9 @@ export function createEditorInstance(props: {
setCurrentEditor(null);
}

if (containerRef) {
containerRef.innerHTML = "";
const el = containerRef();
if (el) {
el.innerHTML = "";
}
});

Expand All @@ -521,7 +560,7 @@ export function createEditorInstance(props: {
monaco: currentMonaco,
containerRef,
setContainerRef: (el: HTMLDivElement) => {
containerRef = el;
setContainerRef(el);
},
isLoading,
activeFile,
Expand Down
15 changes: 14 additions & 1 deletion src/context/editor/EditorProvider.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 (
<EditorContext.Provider
value={{
Expand Down
9 changes: 7 additions & 2 deletions src/context/editor/fileOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ export function createFileOperations(
const openFile = async (path: string, groupId?: string) => {
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);
Expand All @@ -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<string>("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()}`;
Expand Down Expand Up @@ -86,6 +90,7 @@ export function createFileOperations(
);
} finally {
setState("isOpening", false);
console.warn("[openFile] isOpening set to FALSE (finally block)");
}
};

Expand Down
Loading