refactor(app): resolve file panels once per pane

This commit is contained in:
Mohamed Boudra
2026-10-19 07:39:07 +02:00
parent ebd1b2ca63
commit b405f0ddb5
5 changed files with 45 additions and 34 deletions
+11 -7
View File
@@ -7,7 +7,7 @@ import { StyleSheet, UnistylesRuntime, withUnistyles } from "react-native-unisty
import { useTranslation } from "react-i18next";
import { useIsCompactFormFactor } from "@/constants/layout";
import { useSessionStore, type ExplorerFile } from "@/stores/session-store";
import { FilePanelPreview, resolveFilePreviewPanel } from "@/file-panels";
import { FilePanelPreview, resolveFilePreviewPanel, type FilePanel } from "@/file-panels";
import { useAttachmentPreviewUrl } from "@/attachments/use-attachment-preview-url";
import { getFileNameFromPath } from "@/attachments/utils";
import { resolveFilePreviewReadTarget } from "@/file-explorer/preview-target";
@@ -37,6 +37,7 @@ const foregroundMutedColorMapping = (theme: Theme) => ({
});
interface FilePreviewBodyProps {
panel: FilePanel | null;
preview: ExplorerFile | null;
mode?: "preview" | "source";
isLoading: boolean;
@@ -121,6 +122,7 @@ function TooLargeSource({ size }: { size?: number }) {
}
function FilePreviewBody({
panel,
preview,
mode,
isLoading,
@@ -134,7 +136,6 @@ function FilePreviewBody({
}: FilePreviewBodyProps) {
const { t } = useTranslation();
const filePath = location.path;
const { settings } = useAppSettings();
if (isLoading && !preview) {
return (
@@ -153,11 +154,6 @@ function FilePreviewBody({
);
}
const panel = resolveFilePreviewPanel({
preview,
path: filePath,
associations: settings.filePanelAssociations,
});
const showSource = preview.kind === "text" && (Boolean(location.lineStart) || mode === "source");
if (panel && !showSource) {
return (
@@ -274,6 +270,7 @@ export function FilePane({
return (
<FilePanePresentation
panel={panel}
serverId={serverId}
workspaceRoot={normalizedWorkspaceRoot}
client={client}
@@ -312,6 +309,7 @@ function isEditableTextFile(input: {
}
function FilePanePresentation({
panel,
serverId,
workspaceRoot,
client,
@@ -334,6 +332,7 @@ function FilePanePresentation({
navigationRevision,
imagePreviewUri,
}: {
panel: FilePanel | null;
serverId: string;
workspaceRoot: string;
client: DaemonClient | null;
@@ -369,6 +368,7 @@ function FilePanePresentation({
if (editable && client && readTarget && preview?.kind === "text") {
return (
<EditableFilePane
panel={panel}
key={`${serverId}:${readTarget.cwd}:${readTarget.path}`}
serverId={serverId}
workspaceRoot={workspaceRoot}
@@ -421,6 +421,7 @@ function FilePanePresentation({
/>
) : null}
<FilePreviewBody
panel={panel}
preview={preview}
mode={previewMode}
isLoading={isLoading}
@@ -437,6 +438,7 @@ function FilePanePresentation({
}
function EditableFilePane({
panel,
serverId,
workspaceRoot,
client,
@@ -454,6 +456,7 @@ function EditableFilePane({
location,
navigationRevision,
}: {
panel: FilePanel | null;
serverId: string;
workspaceRoot: string;
client: DaemonClient;
@@ -601,6 +604,7 @@ function EditableFilePane({
/>
) : (
<FilePreviewBody
panel={panel}
preview={renderedPreview}
mode={mode}
isLoading={isLoading}
+22 -14
View File
@@ -50,21 +50,29 @@ describe("filePreviewRenderKind", () => {
});
describe("built-in file panels", () => {
it("keeps image classification independent of extension and associations", () => {
const resolution = resolveFilePanel({
file: { path: "/capture", kind: "image", size: 512, hasBytes: true },
candidates: builtinFilePanels,
associations: { "ext:": "builtin:markdown" },
});
expect(resolution.panel?.id).toBe("builtin:image");
expect(
resolveFilePanel({
file: { path: "capture.png", kind: "binary", size: 512, hasBytes: true },
it.each(["png", "jpg", "jpeg", "gif", "webp", "svg"])(
"registers the image extension %s",
(extension) => {
const resolution = resolveFilePanel({
file: {
path: `/capture.${extension.toUpperCase()}`,
kind: "image",
size: 512,
hasBytes: true,
},
candidates: builtinFilePanels,
associations: {},
}).panel,
).toBeNull();
});
associations: { [`ext:${extension}`]: "builtin:markdown" },
});
expect(resolution.panel?.id).toBe("builtin:image");
expect(
resolveFilePanel({
file: { path: "capture.png", kind: "binary", size: 512, hasBytes: false },
candidates: builtinFilePanels,
associations: {},
}).panel,
).toBeNull();
},
);
it("does not impose a new preview size limit on built-in text panels", () => {
expect(
+10 -10
View File
@@ -1,12 +1,14 @@
import { filePanelExtension, type FilePanelCandidate } from "./resolve";
const markdownPanel = {
id: "builtin:markdown",
files: { extensions: ["md", "markdown"] },
document: "text",
maxBytes: Infinity,
} as const satisfies FilePanelCandidate;
export const builtinFilePanels = [
{
id: "builtin:markdown",
files: { extensions: ["md", "markdown"] },
document: "text",
maxBytes: Infinity,
},
markdownPanel,
{
id: "builtin:html",
files: { extensions: ["html", "htm"] },
@@ -15,14 +17,12 @@ export const builtinFilePanels = [
},
{
id: "builtin:image",
files: { extensions: [], kind: "image" },
files: { extensions: ["png", "jpg", "jpeg", "gif", "webp", "svg"] },
document: "bytes",
maxBytes: Infinity,
},
] as const satisfies readonly FilePanelCandidate[];
export function isRenderedMarkdownFile(path: string): boolean {
return builtinFilePanels[0].files.extensions.some(
(extension) => extension === filePanelExtension(path),
);
return markdownPanel.files.extensions.some((extension) => extension === filePanelExtension(path));
}
+2
View File
@@ -6,6 +6,8 @@ export { resolveFilePanel, type FilePanelCandidate, type FilePanelFile } from ".
export { builtinFilePanels, isRenderedMarkdownFile } from "./catalog";
export { FilePanelPreview } from "./builtin";
export type FilePanel = (typeof builtinFilePanels)[number];
export function resolveFilePreviewPanel(input: {
preview: ExplorerFile | null;
path: string;
-3
View File
@@ -8,8 +8,6 @@ export interface FilePanelFile {
export interface FilePanelSelector {
extensions: readonly string[];
filenames?: readonly string[];
/** Image classification comes from the file read, independent of its extension. */
kind?: "image";
}
export interface FilePanelCandidate {
@@ -36,7 +34,6 @@ function filePanelBasename(path: string): string {
}
function matchesSelector(file: FilePanelFile, selector: FilePanelSelector): boolean {
if (selector.kind) return selector.kind === file.kind;
if (selector.filenames?.includes(filePanelBasename(file.path))) return true;
const extension = filePanelExtension(file.path);
return selector.extensions.some((entry) => entry.toLowerCase() === extension);