Skip to content

Commit ed0b602

Browse files
committed
fix(frontend): preserve empty file preview edits
Treat empty contents as valid so edits to an initially empty text file survive file switches, and use a nullish fallback when restoring a saved draft so a cleared file is not overwritten by stale server contents. Correct the max_bytes docstring to match its passthrough behavior.
1 parent 8f0055f commit ed0b602

3 files changed

Lines changed: 59 additions & 5 deletions

File tree

frontend/src/components/editor/file-tree/__tests__/file-viewer.test.tsx

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* Copyright 2026 Marimo. All rights reserved. */
22

3-
import { render, screen, waitFor } from "@testing-library/react";
3+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
44
import { Provider } from "jotai";
55
import type React from "react";
66
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -12,7 +12,19 @@ import { store } from "@/core/state/jotai";
1212
import { FileViewer, MAX_FILE_PREVIEW_BYTES } from "../file-viewer";
1313

1414
vi.mock("@/plugins/impl/code/LazyAnyLanguageCodeMirror", () => ({
15-
LazyAnyLanguageCodeMirror: () => <div data-testid="code-editor" />,
15+
LazyAnyLanguageCodeMirror: ({
16+
value,
17+
onChange,
18+
}: {
19+
value?: string;
20+
onChange?: (value: string) => void;
21+
}) => (
22+
<textarea
23+
data-testid="code-editor"
24+
value={value ?? ""}
25+
onChange={(evt) => onChange?.(evt.target.value)}
26+
/>
27+
),
1628
}));
1729

1830
vi.mock("@/core/wasm/utils", () => ({ isWasm: () => false }));
@@ -93,4 +105,46 @@ describe("FileViewer bounded previews", () => {
93105
});
94106
expect(screen.queryByText(/too large to preview/i)).not.toBeInTheDocument();
95107
});
108+
109+
it("preserves edits to an initially empty text file across remount", async () => {
110+
const emptyFile: FileInfo = {
111+
id: "/workspace/empty.txt",
112+
path: "/workspace/empty.txt",
113+
name: "empty.txt",
114+
isDirectory: false,
115+
isMarimoFile: false,
116+
size: 0,
117+
};
118+
const response: FileDetailsResponse = {
119+
file: emptyFile,
120+
contents: "",
121+
mimeType: "text/plain",
122+
isBase64: false,
123+
isTooLarge: false,
124+
};
125+
const client = MockRequestClient.create({
126+
sendFileDetails: vi.fn().mockResolvedValue(response),
127+
});
128+
store.set(requestClientAtom, client);
129+
130+
const { unmount } = render(
131+
<FileViewer file={emptyFile} onOpenNotebook={vi.fn()} />,
132+
{ wrapper },
133+
);
134+
135+
const editor =
136+
await screen.findByTestId<HTMLTextAreaElement>("code-editor");
137+
fireEvent.change(editor, { target: { value: "draft" } });
138+
unmount();
139+
140+
render(<FileViewer file={emptyFile} onOpenNotebook={vi.fn()} />, {
141+
wrapper,
142+
});
143+
144+
const reopened =
145+
await screen.findByTestId<HTMLTextAreaElement>("code-editor");
146+
await waitFor(() => {
147+
expect(reopened.value).toBe("draft");
148+
});
149+
});
96150
});

frontend/src/components/editor/file-tree/file-viewer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
5757
const mode = getFileRenderMode(details.mimeType, details.isBase64);
5858
if (!details.isTooLarge && mode === "text") {
5959
const contents = details.contents || "";
60-
setInternalValue(unsavedContentsForFile.get(file.path) || contents);
60+
setInternalValue(unsavedContentsForFile.get(file.path) ?? contents);
6161
}
6262
return details;
6363
}, [file.path]);
@@ -87,7 +87,7 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
8787
useEffect(() => {
8888
return () => {
8989
if (
90-
!data?.contents ||
90+
data?.contents == null ||
9191
data.isTooLarge ||
9292
getFileRenderMode(data.mimeType, data.isBase64) !== "text"
9393
) {

marimo/_server/models/files.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class FileListRequest(msgspec.Struct, rename="camel"):
3232
class FileDetailsRequest(msgspec.Struct, rename="camel"):
3333
# The path of the file or directory
3434
path: str
35-
# Optional caller limit; the HTTP endpoint clamps this to its hard ceiling.
35+
# Optional cap on bytes read from disk; unbounded when omitted.
3636
max_bytes: int | None = None
3737

3838

0 commit comments

Comments
 (0)