Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ vi.mock(import("~/assets/assetUrls"), async (original) => ({
// The real source view highlights through a worker pool; the text it receives is what matters here.
// Never load the original: its highlighter graph can outlast waitFor on a cold CI browser.
vi.mock(import("./ReadOnlySourcePreview"), () => ({
default: ({ text }: { text: string }) => <pre>{text}</pre>,
default: ({ text, wordWrap }: { text: string; wordWrap?: boolean }) => (
<pre data-word-wrap={String(wordWrap)}>{text}</pre>
),
}));

// The Word engine compiles about 30 MB of WebAssembly in a fresh worker per document, which took
Expand All @@ -41,6 +43,7 @@ vi.mock(import("./office/WordDocument"), async () => {
};
});

import { getClientSettings } from "~/hooks/useSettings";
import { AttachmentFilePreview } from "./AttachmentFilePreview";
import LargeTextViewer from "./LargeTextViewer";
import VirtualTablePreview from "./VirtualTablePreview";
Expand Down Expand Up @@ -492,6 +495,7 @@ describe("large text preview recovery", () => {
workspaceMutationId={mutation}
revealLine={null}
revealRequestId={0}
wordWrap
/>
</div>
);
Expand Down Expand Up @@ -2341,6 +2345,22 @@ it("toggles a Mermaid attachment between diagram and source through the lazy pre
expect(screen.container.querySelector('[role="alert"]')).toBeNull();
});

it("unwraps one text attachment without turning off the word wrap setting", async () => {
const file = new Blob(["a long line"], { type: "text/plain" });
await using screen = await render(
<AttachmentFilePreview
name="notes.txt"
mimeType={file.type}
sizeBytes={file.size}
file={file}
/>,
);
await screen.getByRole("button", { name: "Disable word wrap" }).click();
await expect.element(screen.getByRole("button", { name: "Enable word wrap" })).toBeVisible();
expect(getClientSettings().wordWrap).toBe(true);
expect(screen.container.querySelector("pre")?.dataset.wordWrap).toBe("false");
});

it("mounts a bounded set of saved notebook outputs as the reader scrolls", async () => {
const notebook = {
nbformat: 4,
Expand Down
13 changes: 8 additions & 5 deletions apps/web/src/components/files/AttachmentFilePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { MorphIcon } from "~/components/MorphIcon";
import { ScrollArea } from "~/components/ui/scroll-area";
import { toastManager } from "~/components/ui/toast";
import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
import { useClientSettings, updateClientSettings } from "~/hooks/useSettings";
import { useClientSettings } from "~/hooks/useSettings";
import { cn } from "~/lib/utils";

import { BinaryFilePreview } from "./BinaryFilePreview";
Expand Down Expand Up @@ -55,7 +55,7 @@ const SourcePreview = lazy(() => import("./ReadOnlySourcePreview"));

const STALE_URL_MS = 5 * 60_000;

export function ReadOnlySourcePreview(props: { name: string; text: string }) {
export function ReadOnlySourcePreview(props: { name: string; text: string; wordWrap?: boolean }) {
return (
<Suspense fallback={<FileSurfaceLoading className="p-4" />}>
<SourcePreview {...props} />
Expand Down Expand Up @@ -214,7 +214,9 @@ export function AttachmentFilePreview(props: {
return () => controller.abort();
}, [url, needsText, streamsText, revision, props.sizeBytes, props.file, refresh]);
const failure = error ?? (needsText ? contentError : null);
const wordWrap = useClientSettings((settings) => settings.wordWrap);
const defaultWordWrap = useClientSettings((settings) => settings.wordWrap);
const [wordWrapOverride, setWordWrapOverride] = useState<boolean | null>(null);
const wordWrap = wordWrapOverride ?? defaultWordWrap;
const showsRawText =
failure === null &&
needsText &&
Expand Down Expand Up @@ -272,6 +274,7 @@ export function AttachmentFilePreview(props: {
onRetry={() => setRevision((value) => value + 1)}
delimiter={rendered ? delimiter : null}
copyContents={copyContents}
wordWrap={wordWrap}
/>
</Suspense>
) : !url || (needsText && !content) ? (
Expand All @@ -292,7 +295,7 @@ export function AttachmentFilePreview(props: {
/>
</ScrollArea>
) : (
<ReadOnlySourcePreview name={props.name} text={content.text} />
<ReadOnlySourcePreview name={props.name} text={content.text} wordWrap={wordWrap} />
)
) : kind === "archive" || kind === "sqlite" ? (
<BinaryFilePreview
Expand Down Expand Up @@ -387,7 +390,7 @@ export function AttachmentFilePreview(props: {
<FileSurfaceAction
label={wordWrap ? "Disable word wrap" : "Enable word wrap"}
pressed={wordWrap}
onPress={() => updateClientSettings({ wordWrap: !wordWrap })}
onPress={() => setWordWrapOverride(!wordWrap)}
>
<WrapTextIcon className="size-3.5" />
</FileSurfaceAction>
Expand Down
12 changes: 9 additions & 3 deletions apps/web/src/components/files/FilePreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import { MediaVideoPlayer } from "~/components/media/MediaVideoPlayer";
import { MediaActions, type MediaActionSource } from "~/components/media/MediaActions";
import { MorphIcon } from "~/components/MorphIcon";
import { useRemoteOpenState } from "~/remoteOpen";
import { useClientSettings, updateClientSettings } from "~/hooks/useSettings";
import { useClientSettings } from "~/hooks/useSettings";
import { useTheme } from "~/hooks/useTheme";
import { getLocalStorageItem, setLocalStorageItem, useLocalStorage } from "~/hooks/useLocalStorage";
import { resolveDiffThemeName } from "~/lib/diffRendering";
Expand Down Expand Up @@ -996,7 +996,9 @@ export default function FilePreviewPanel({
attachment === undefined ? resolveFilePreviewPath(requestedPath, cwd) : requestedPath;
const draft = typeof composerDraftTarget === "string";
const { resolvedTheme } = useTheme();
const wordWrap = useClientSettings((settings) => settings.wordWrap);
const defaultWordWrap = useClientSettings((settings) => settings.wordWrap);
const [wordWrapOverride, setWordWrapOverride] = useState<boolean | null>(null);
const wordWrap = wordWrapOverride ?? defaultWordWrap;
const canOperatePreview = useEnvironmentScope(environmentId, AuthPreviewOperateScope);
const primaryEnvironmentId = usePrimaryEnvironmentId();
const remoteOpenState = useRemoteOpenState(environmentId);
Expand Down Expand Up @@ -1291,7 +1293,7 @@ export default function FilePreviewPanel({
<FileSurfaceAction
label={wordWrap ? "Disable word wrap" : "Enable word wrap"}
pressed={wordWrap}
onPress={() => updateClientSettings({ wordWrap: !wordWrap })}
onPress={() => setWordWrapOverride(!wordWrap)}
>
<WrapTextIcon className="size-3.5" />
</FileSurfaceAction>
Expand Down Expand Up @@ -1403,6 +1405,7 @@ export default function FilePreviewPanel({
workspaceMutationId={workspaceMutationId}
revealLine={revealLine}
revealRequestId={revealRequestId}
wordWrap={wordWrap}
/>
) : relativePath &&
absolutePath &&
Expand Down Expand Up @@ -1488,6 +1491,7 @@ export default function FilePreviewPanel({
text={file.data.contents}
cacheKey={projectFileCacheKey(cwd, relativePath, file.data.contents)}
onPostRender={onFilePostRender}
wordWrap={wordWrap}
/>
) : (
<DiffWorkerPoolProvider>
Expand Down Expand Up @@ -1557,6 +1561,7 @@ export function WorkspaceLargeTextPreview(props: {
workspaceMutationId: string | null;
revealLine: number | null;
revealRequestId: number;
wordWrap: boolean;
}) {
const resource = {
_tag: "media-file",
Expand Down Expand Up @@ -1659,6 +1664,7 @@ export function WorkspaceLargeTextPreview(props: {
delimiter={props.delimiter}
revealLine={props.revealLine}
revealRequestId={props.revealRequestId}
wordWrap={props.wordWrap}
/>
</Suspense>
</>
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/files/LargeTextViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export default function LargeTextViewer(props: {
readonly delimiter: "," | "\t" | null;
readonly revealLine?: number | null;
readonly revealRequestId?: number;
/** A preview's own wrap toggle; omitted follows the word wrap setting. */
readonly wordWrap?: boolean;
}) {
const [meta, setMeta] = useState<Extract<TextPreviewResult, { type: "meta" }> | null>(null);
const [pages, setPages] = useState(new Map<number, string[][]>());
Expand All @@ -46,7 +48,8 @@ export default function LargeTextViewer(props: {
reject: (cause: Error) => void;
} | null>(null);
const pending = useRef(new Set<number>());
const wordWrap = useClientSettings((settings) => settings.wordWrap);
const defaultWordWrap = useClientSettings((settings) => settings.wordWrap);
const wordWrap = props.wordWrap ?? defaultWordWrap;
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- Own the prefix fetch and indexing worker, cancelling requests and pending copies when the source changes or unmounts.
useEffect(() => {
// oxlint-disable-next-line react/set-state-in-effect -- A replacement source must discard the previous worker's error and pages.
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/files/ReadOnlySourcePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ export default function ReadOnlySourcePreview(props: {
readonly text: string;
readonly cacheKey?: string;
readonly onPostRender?: FileOptions<unknown, undefined>["onPostRender"];
/** A preview's own wrap toggle; omitted follows the word wrap setting. */
readonly wordWrap?: boolean;
}) {
const resolvedTheme = useRenderedAppearance();
const wordWrap = useClientSettings((settings) => settings.wordWrap);
const defaultWordWrap = useClientSettings((settings) => settings.wordWrap);
const wordWrap = props.wordWrap ?? defaultWordWrap;
return (
<DiffWorkerPoolProvider>
<Virtualizer
Expand Down
Loading