diff --git a/src/components/code-editor/hooks/useCodeEditorDocument.ts b/src/components/code-editor/hooks/useCodeEditorDocument.ts index 74f928d6..dda02887 100644 --- a/src/components/code-editor/hooks/useCodeEditorDocument.ts +++ b/src/components/code-editor/hooks/useCodeEditorDocument.ts @@ -44,13 +44,17 @@ export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocume // Natively previewable media (image/pdf/audio/video) is rendered by // CodeEditorMediaPreview, so there is nothing to read as text here. + // Clear any buffer left over from a previously opened text file so a + // stray save can't write stale content over the binary file. if (getPreviewKind(file.name)) { + setContent(''); setLoading(false); return; } // Check if file is binary by extension if (isBinaryFile(file.name)) { + setContent(''); setIsBinary(true); setLoading(false); return; @@ -87,6 +91,12 @@ export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocume }, [file.diffInfo, file.name, fileDiffNewString, fileDiffOldString, fileName, filePath, fileProjectId]); const handleSave = useCallback(async () => { + // Preview-only and binary files have no editable text buffer; never write + // them back (e.g. via Cmd/Ctrl+S) or we'd corrupt the file on disk. + if (previewKind || isBinaryFile(fileName)) { + return; + } + setSaving(true); setSaveError(null); @@ -120,7 +130,7 @@ export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocume } finally { setSaving(false); } - }, [content, filePath, fileProjectId]); + }, [content, filePath, fileProjectId, previewKind, fileName]); const handleDownload = useCallback(() => { const blob = new Blob([content], { type: 'text/plain' }); diff --git a/src/components/code-editor/utils/previewableFile.ts b/src/components/code-editor/utils/previewableFile.ts index 464cad81..0071de4e 100644 --- a/src/components/code-editor/utils/previewableFile.ts +++ b/src/components/code-editor/utils/previewableFile.ts @@ -5,27 +5,59 @@ export type PreviewKind = 'image' | 'pdf' | 'video' | 'audio'; -// Formats browsers can decode in . -const IMAGE_EXTENSIONS = new Set([ - 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp', 'avif', 'apng', -]); +// Single source of truth: every extension the browser can preview, mapped to the +// MIME type we apply when the server response has a missing/generic Content-Type. +// The preview kind is derived from the MIME type so the two never drift apart. +// Formats browsers generally can't play (avi, mkv, flv, wmv) are intentionally +// absent and keep the binary fallback. +const EXTENSION_MIME: Record = { + // Images + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + svg: 'image/svg+xml', + webp: 'image/webp', + ico: 'image/x-icon', + bmp: 'image/bmp', + avif: 'image/avif', + apng: 'image/apng', + // PDF + pdf: 'application/pdf', + // Video + mp4: 'video/mp4', + webm: 'video/webm', + ogv: 'video/ogg', + mov: 'video/quicktime', + m4v: 'video/x-m4v', + // Audio + mp3: 'audio/mpeg', + wav: 'audio/wav', + m4a: 'audio/mp4', + aac: 'audio/aac', + flac: 'audio/flac', + opus: 'audio/opus', + oga: 'audio/ogg', + ogg: 'audio/ogg', + weba: 'audio/webm', +}; -const PDF_EXTENSIONS = new Set(['pdf']); +const extensionOf = (filename: string): string => filename.split('.').pop()?.toLowerCase() ?? ''; -// Container/codec combos broadly playable in