(null);
React.useEffect(() => {
let cancelled = false;
const selectedPath = selectedFile?.path;
if (!selectedPath || staticLanguageExtension) {
setDynamicLanguageExtension(null);
return;
}
setDynamicLanguageExtension(null);
void loadLanguageByExtension(selectedPath).then((extension) => {
if (!cancelled) {
setDynamicLanguageExtension(extension);
}
});
return () => {
cancelled = true;
};
}, [selectedFile?.path, staticLanguageExtension]);
React.useEffect(() => {
if (!canEdit && textViewMode === 'edit') {
setTextViewMode('view');
}
}, [canEdit, textViewMode]);
const MD_VIEWER_MODE_KEY = 'openchamber:files:md-viewer-mode';
const HTML_VIEWER_MODE_KEY = 'openchamber:files:html-viewer-mode';
const JSON_VIEWER_MODE_KEY = 'openchamber:files:json-viewer-mode';
React.useEffect(() => {
const selectedPath = selectedFile?.path;
if (!selectedPath) {
return;
}
const defaultMode: TextViewMode = settingsDefaultFileViewerPreview ? 'view' : 'edit';
setTextViewMode(textViewModeByPathRef.current[selectedPath] ?? defaultMode);
// Respect per-type localStorage preference when available,
// falling back to the setting-derived default when nothing is stored.
let mdDefault: PreviewViewMode = settingsDefaultFileViewerPreview ? 'preview' : 'edit';
try {
const stored = localStorage.getItem(MD_VIEWER_MODE_KEY);
if (stored === 'preview' || stored === 'edit') {
mdDefault = stored;
}
} catch {
// Ignore localStorage errors
}
setMdViewMode(mdViewModeByPathRef.current[selectedPath] ?? mdDefault);
let htmlDefault: PreviewViewMode = settingsDefaultFileViewerPreview ? 'preview' : 'edit';
try {
const stored = localStorage.getItem(HTML_VIEWER_MODE_KEY);
if (stored === 'preview' || stored === 'edit') {
htmlDefault = stored;
}
} catch {
// Ignore localStorage errors
}
setHtmlViewMode(htmlViewModeByPathRef.current[selectedPath] ?? htmlDefault);
setDrawioViewMode(drawioViewModeByPathRef.current[selectedPath] ?? 'preview');
let jsonDefault: 'tree' | 'text' = settingsDefaultFileViewerPreview ? 'tree' : 'text';
try {
const stored = localStorage.getItem(JSON_VIEWER_MODE_KEY);
if (stored === 'tree' || stored === 'text') {
jsonDefault = stored;
}
} catch {
// Ignore localStorage errors
}
setJsonViewMode(jsonDefault);
}, [selectedFile?.path, settingsDefaultFileViewerPreview]);
const saveTextViewMode = React.useCallback((mode: TextViewMode) => {
const selectedPath = selectedFile?.path;
if (selectedPath) {
textViewModeByPathRef.current[selectedPath] = mode;
}
setTextViewMode(mode);
}, [selectedFile?.path]);
const saveMdViewMode = React.useCallback((mode: PreviewViewMode) => {
const selectedPath = selectedFile?.path;
if (selectedPath) {
mdViewModeByPathRef.current[selectedPath] = mode;
}
setMdViewMode(mode);
try {
localStorage.setItem(MD_VIEWER_MODE_KEY, mode);
} catch {
// Ignore localStorage errors
}
}, [selectedFile?.path]);
const getMdViewMode = React.useCallback((): PreviewViewMode => {
return mdViewMode;
}, [mdViewMode]);
const saveJsonViewMode = React.useCallback((mode: 'tree' | 'text') => {
setJsonViewMode(mode);
try {
localStorage.setItem(JSON_VIEWER_MODE_KEY, mode);
} catch {
// Ignore localStorage errors
}
}, []);
const saveHtmlViewMode = React.useCallback((mode: PreviewViewMode) => {
const selectedPath = selectedFile?.path;
if (selectedPath) {
htmlViewModeByPathRef.current[selectedPath] = mode;
}
setHtmlViewMode(mode);
try {
localStorage.setItem(HTML_VIEWER_MODE_KEY, mode);
} catch {
// Ignore localStorage errors
}
}, [selectedFile?.path]);
const saveDrawioViewMode = React.useCallback((mode: PreviewViewMode) => {
const selectedPath = selectedFile?.path;
if (selectedPath) {
drawioViewModeByPathRef.current[selectedPath] = mode;
}
if (diagramAutoSaveTimerRef.current) {
clearTimeout(diagramAutoSaveTimerRef.current);
diagramAutoSaveTimerRef.current = null;
}
if (pendingDrawioPreviewFrameRef.current !== null) {
cancelAnimationFrame(pendingDrawioPreviewFrameRef.current);
pendingDrawioPreviewFrameRef.current = null;
}
if (mode === 'edit') {
setDraftContent(diagramXmlRef.current || fileContent);
setDrawioViewMode(mode);
} else {
diagramXmlRef.current = draftContent;
const pathAtToggle = selectedPath;
setDrawioViewMode('edit');
pendingDrawioPreviewFrameRef.current = requestAnimationFrame(() => {
pendingDrawioPreviewFrameRef.current = requestAnimationFrame(() => {
pendingDrawioPreviewFrameRef.current = null;
if (root && pathAtToggle && useFilesViewTabsStore.getState().byRoot[root]?.selectedPath !== pathAtToggle) {
return;
}
setDrawioRemountNonce((value) => value + 1);
setDrawioViewMode('preview');
});
});
return;
}
}, [draftContent, fileContent, root, selectedFile?.path]);
const saveDiagramXml = React.useCallback(async (path: string, xml: string) => {
if (!files.writeFile || xml === diagramSavedXmlRef.current) {
return false;
}
const result = await files.writeFile(path, xml);
if (!result?.success) {
toast.error(t('filesView.toast.writeFileFailed'));
return false;
}
diagramXmlRef.current = xml;
diagramSavedXmlRef.current = xml;
setDraftContent(xml);
const stat = await readFileStat(path, selectedFileReadOptions).catch(() => null);
if (stat) {
lastLoadedFileStatRef.current = stat;
}
return true;
}, [files, readFileStat, selectedFileReadOptions, t]);
React.useEffect(() => {
return () => {
if (diagramAutoSaveTimerRef.current) {
clearTimeout(diagramAutoSaveTimerRef.current);
diagramAutoSaveTimerRef.current = null;
}
if (pendingDrawioPreviewFrameRef.current !== null) {
cancelAnimationFrame(pendingDrawioPreviewFrameRef.current);
pendingDrawioPreviewFrameRef.current = null;
}
};
}, [drawioViewMode, selectedFile?.path]);
const handleDiagramChange = React.useCallback((xml: string) => {
diagramXmlRef.current = xml;
if (!selectedFile?.path || drawioViewMode !== 'preview' || !files.writeFile) {
return;
}
if (diagramAutoSaveTimerRef.current) {
clearTimeout(diagramAutoSaveTimerRef.current);
}
const path = selectedFile.path;
diagramAutoSaveTimerRef.current = setTimeout(() => {
diagramAutoSaveTimerRef.current = null;
void saveDiagramXml(path, xml).then((saved) => {
if (!saved) return;
setDiagramSaved(true);
setTimeout(() => setDiagramSaved(false), 1500);
}).catch((error) => {
toast.error(error instanceof Error ? error.message : t('filesView.toast.saveFailed'));
});
}, AUTO_SAVE_DELAY);
}, [drawioViewMode, files.writeFile, saveDiagramXml, selectedFile?.path, t]);
const diagramEditorXml = React.useMemo(() => {
if (!isDrawio) {
return fileContent;
}
return diagramXmlRef.current || draftContent || fileContent;
}, [draftContent, fileContent, isDrawio]);
const getHtmlViewMode = React.useCallback((): PreviewViewMode => {
return htmlViewMode;
}, [htmlViewMode]);
React.useEffect(() => {
const applyDefaultFileViewerMode = (enabled: boolean) => {
const textMode: TextViewMode = enabled ? 'view' : 'edit';
const previewMode: PreviewViewMode = enabled ? 'preview' : 'edit';
const nextJsonMode: 'tree' | 'text' = enabled ? 'tree' : 'text';
for (const path of openPaths) {
textViewModeByPathRef.current[path] = textMode;
if (isMarkdownFile(path)) {
mdViewModeByPathRef.current[path] = previewMode;
}
if (isHtmlFile(path)) {
htmlViewModeByPathRef.current[path] = previewMode;
}
}
setTextViewMode(textMode);
setMdViewMode(previewMode);
setHtmlViewMode(previewMode);
setJsonViewMode(nextJsonMode);
try {
localStorage.setItem(MD_VIEWER_MODE_KEY, previewMode);
localStorage.setItem(HTML_VIEWER_MODE_KEY, previewMode);
localStorage.setItem(JSON_VIEWER_MODE_KEY, nextJsonMode);
} catch {
// Ignore localStorage errors
}
};
const handleFileViewerModeChanged = (event: Event) => {
const enabled = Boolean((event as CustomEvent<{ enabled?: boolean }>).detail?.enabled);
applyDefaultFileViewerMode(enabled);
};
window.addEventListener('openchamber:file-viewer-preview-mode-changed', handleFileViewerModeChanged);
return () => {
window.removeEventListener('openchamber:file-viewer-preview-mode-changed', handleFileViewerModeChanged);
};
}, [openPaths]);
React.useEffect(() => {
if (!pendingFileNavigation || !root) {
return;
}
const scheduleNavigationRetry = () => {
if (typeof window === 'undefined') {
return;
}
if (pendingNavigationRafRef.current !== null) {
return;
}
pendingNavigationRafRef.current = window.requestAnimationFrame(() => {
pendingNavigationRafRef.current = null;
setEditorViewReadyNonce((value) => value + 1);
});
};
const isEditorSyncedWithDraft = (view: EditorView, expectedContent: string): boolean => {
if (view.state.doc.length !== expectedContent.length) {
return false;
}
if (expectedContent.length === 0) {
return true;
}
const sampleSize = Math.min(128, expectedContent.length);
const startSample = view.state.sliceDoc(0, sampleSize);
if (startSample !== expectedContent.slice(0, sampleSize)) {
return false;
}
const endFrom = Math.max(0, expectedContent.length - sampleSize);
const endSample = view.state.sliceDoc(endFrom, expectedContent.length);
return endSample === expectedContent.slice(endFrom);
};
const targetPath = normalizePath(pendingFileNavigation.path);
if (!targetPath) {
setPendingFileNavigation(null);
pendingNavigationCycleRef.current = { key: '', attempts: 0 };
return;
}
const navigationKey = `${targetPath}:${pendingFileNavigation.line}:${pendingFileNavigation.column ?? 1}`;
if (pendingNavigationCycleRef.current.key !== navigationKey) {
pendingNavigationCycleRef.current = { key: navigationKey, attempts: 0 };
}
if (selectedFile?.path !== targetPath) {
if (confirmDiscardOpen) {
return;
}
void handleSelectFile(toFileNode(targetPath));
return;
}
if (fileLoading || loadedFilePath !== targetPath) {
return;
}
if (fileError || isSelectedImage) {
setPendingFileNavigation(null);
pendingNavigationCycleRef.current = { key: '', attempts: 0 };
return;
}
if (!canEdit) {
return;
}
if (textViewMode !== 'edit') {
setTextViewMode('edit');
return;
}
const view = editorViewRef.current;
if (!view) {
scheduleNavigationRetry();
return;
}
if (!isEditorSyncedWithDraft(view, draftContent)) {
scheduleNavigationRetry();
return;
}
const targetLineNumber = Math.max(1, Math.min(pendingFileNavigation.line, view.state.doc.lines));
const targetLine = view.state.doc.line(targetLineNumber);
const targetColumn = Math.max(1, pendingFileNavigation.column || 1);
const lineLength = Math.max(0, targetLine.to - targetLine.from);
const clampedColumnOffset = Math.min(lineLength, targetColumn - 1);
const targetPosition = targetLine.from + clampedColumnOffset;
const isAtTarget = view.state.selection.main.head === targetPosition;
const shouldDispatch = !isAtTarget || pendingNavigationCycleRef.current.attempts === 0;
if (shouldDispatch) {
pendingNavigationCycleRef.current.attempts += 1;
view.dispatch({
selection: { anchor: targetPosition },
effects: EditorView.scrollIntoView(targetPosition, { y: 'center' }),
});
view.focus();
scheduleNavigationRetry();
return;
}
if (typeof window !== 'undefined') {
window.requestAnimationFrame(() => {
const syncedView = editorViewRef.current;
if (!syncedView) {
return;
}
syncedView.dispatch({
selection: { anchor: targetPosition },
effects: EditorView.scrollIntoView(targetPosition, { y: 'center' }),
});
syncedView.focus();
});
}
setPendingFileNavigation(null);
pendingNavigationCycleRef.current = { key: '', attempts: 0 };
}, [
canEdit,
confirmDiscardOpen,
draftContent,
editorViewReadyNonce,
fileError,
fileLoading,
isSelectedImage,
loadedFilePath,
handleSelectFile,
pendingFileNavigation,
root,
selectedFile?.path,
setPendingFileNavigation,
textViewMode,
toFileNode,
]);
React.useEffect(() => {
if (!pendingFileFocusPath || !root) {
return;
}
const targetPath = normalizePath(pendingFileFocusPath);
if (!targetPath) {
setPendingFileFocusPath(null);
return;
}
if (selectedFile?.path !== targetPath) {
if (confirmDiscardOpen) {
return;
}
void handleSelectFile(toFileNode(targetPath));
return;
}
if (fileLoading || loadedFilePath !== targetPath || fileError || isSelectedImage) {
return;
}
if (canEdit && textViewMode === 'edit') {
const view = editorViewRef.current;
if (!view) {
return;
}
view.focus();
}
setPendingFileFocusPath(null);
}, [
canEdit,
confirmDiscardOpen,
fileError,
fileLoading,
handleSelectFile,
isSelectedImage,
loadedFilePath,
pendingFileFocusPath,
root,
selectedFile?.path,
setPendingFileFocusPath,
textViewMode,
toFileNode,
]);
const nudgeEditorSelectionAboveKeyboard = React.useCallback((view: EditorView | null) => {
if (!isMobile || !view || !view.hasFocus || typeof window === 'undefined') {
return;
}
const viewport = window.visualViewport;
if (!viewport) {
return;
}
const layoutHeight = document.documentElement.clientHeight || window.innerHeight;
const occludedBottom = Math.max(0, layoutHeight - (viewport.offsetTop + viewport.height));
if (occludedBottom <= 0) {
return;
}
const head = view.state.selection.main.head;
const cursorRect = view.coordsAtPos(head);
if (!cursorRect) {
return;
}
const visibleBottom = Math.round(viewport.offsetTop + viewport.height);
const clearance = 20;
const overlap = cursorRect.bottom + clearance - visibleBottom;
if (overlap <= 0) {
return;
}
view.scrollDOM.scrollTop += overlap;
}, [isMobile]);
React.useEffect(() => {
if (!isMobile || typeof window === 'undefined') {
return;
}
const runNudge = () => {
window.requestAnimationFrame(() => {
nudgeEditorSelectionAboveKeyboard(editorViewRef.current);
});
};
const viewport = window.visualViewport;
viewport?.addEventListener('resize', runNudge);
viewport?.addEventListener('scroll', runNudge, { passive: true });
document.addEventListener('selectionchange', runNudge);
return () => {
viewport?.removeEventListener('resize', runNudge);
viewport?.removeEventListener('scroll', runNudge);
document.removeEventListener('selectionchange', runNudge);
};
}, [isMobile, nudgeEditorSelectionAboveKeyboard]);
React.useEffect(() => {
if (!canEdit || textViewMode !== 'edit' || isMobile) {
return;
}
const goToLineCombo = getEffectiveShortcutCombo('open_go_to_line', shortcutOverrides);
const handleKeyDown = (event: KeyboardEvent) => {
const target = event.target as Element | null;
if (target?.closest('[role="dialog"]')) {
return;
}
const isEditorTarget = Boolean(target?.closest('.cm-editor'));
const isTypingTarget = Boolean(
target?.closest('input, textarea, [contenteditable="true"], [role="textbox"]')
);
if (isTypingTarget && !isEditorTarget) {
return;
}
const activeElement = document.activeElement as Element | null;
const editorHasFocus = Boolean(activeElement?.closest('.cm-editor'));
if (!editorHasFocus) {
return;
}
if (eventMatchesShortcut(event, goToLineCombo)) {
event.preventDefault();
setIsGoToLineOpen(true);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [canEdit, isMobile, shortcutOverrides, textViewMode]);
const editorExtensions = React.useMemo(() => {
if (!selectedFile?.path) {
return [createFlexokiCodeMirrorTheme(currentTheme)];
}
const extensions = [createFlexokiCodeMirrorTheme(currentTheme)];
const language = staticLanguageExtension ?? dynamicLanguageExtension;
if (language) {
extensions.push(language);
}
if (wrapLines) {
extensions.push(EditorView.lineWrapping);
}
if (isMobile) {
extensions.push(EditorView.updateListener.of((update) => {
if (!update.view.hasFocus) {
return;
}
if (!(update.selectionSet || update.focusChanged || update.viewportChanged || update.geometryChanged)) {
return;
}
window.requestAnimationFrame(() => {
nudgeEditorSelectionAboveKeyboard(update.view);
});
}));
}
return extensions;
}, [currentTheme, selectedFile?.path, staticLanguageExtension, dynamicLanguageExtension, wrapLines, isMobile, nudgeEditorSelectionAboveKeyboard]);
const pierreTheme = React.useMemo(
() => ({ light: lightTheme.metadata.id, dark: darkTheme.metadata.id }),
[lightTheme.metadata.id, darkTheme.metadata.id],
);
const imageAssetAuthKey = selectedFile?.path && isSelectedImage && !runtime.isDesktop && !isSelectedSvg
? `${selectedFile.path}|${selectedFileReadOptions.allowOutsideWorkspace ? 'outside' : 'workspace'}|${selectedFileReadOptions.outsideFileGrant ?? ''}`
: '';
React.useEffect(() => {
if (!imageAssetAuthKey) {
setImageAssetAuthReadyKey('');
return;
}
let cancelled = false;
setImageAssetAuthReadyKey('');
void refreshRuntimeUrlAuthToken(getRuntimeApiBaseUrl())
.then((token) => {
if (!cancelled && token) setImageAssetAuthReadyKey(imageAssetAuthKey);
})
.catch(() => {});
return () => {
cancelled = true;
};
}, [imageAssetAuthKey]);
const isImageAssetAuthLoading = Boolean(imageAssetAuthKey && imageAssetAuthReadyKey !== imageAssetAuthKey);
const imageSrc = selectedFile?.path && isSelectedImage
? (runtime.isDesktop
? (isSelectedSvg
? `data:${getImageMimeType(selectedFile.path)};utf8,${encodeURIComponent(fileContent)}`
: desktopImageSrc)
: (isSelectedSvg
? `data:${getImageMimeType(selectedFile.path)};utf8,${encodeURIComponent(fileContent)}`
: imageAssetAuthReadyKey === imageAssetAuthKey ? getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw', {
path: selectedFile.path,
allowOutsideWorkspace: selectedFileReadOptions.allowOutsideWorkspace ? 'true' : undefined,
outsideFileGrant: selectedFileReadOptions.outsideFileGrant,
}) : ''))
: '';
React.useEffect(() => {
let cancelled = false;
const resolveDesktopImage = async () => {
if (!runtime.isDesktop || !selectedFile?.path || !isSelectedImage || isSelectedSvg) {
setDesktopImageSrc('');
return;
}
setFileError(null);
const srcPromise = files.readFileBinary
? files.readFileBinary(selectedFile.path, selectedFileReadOptions).then((result) => result.dataUrl)
: Promise.resolve(getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw', {
path: selectedFile.path,
allowOutsideWorkspace: selectedFileReadOptions.allowOutsideWorkspace ? 'true' : undefined,
outsideFileGrant: selectedFileReadOptions.outsideFileGrant,
}));
await srcPromise
.then((src) => {
if (!cancelled) {
setDesktopImageSrc(src);
setLoadedFilePath(selectedFile.path);
}
})
.catch((error) => {
if (!cancelled) {
setDesktopImageSrc('');
setFileError(error instanceof Error ? error.message : t('filesView.error.readFileFailed'));
setLoadedFilePath(null);
}
})
.finally(() => {
if (!cancelled) {
setFileLoading(false);
}
});
};
void resolveDesktopImage();
return () => {
cancelled = true;
};
}, [files, isSelectedImage, isSelectedSvg, runtime.isDesktop, selectedFile?.path, selectedFileReadOptions, t]);
const handleCloseDialog = React.useCallback(() => setActiveDialog(null), []);
const blockWidgets = React.useMemo(() => {
return buildCodeMirrorCommentWidgets({
drafts: filesFileDrafts,
editingDraftId,
commentText,
selection: lineSelection,
isDragging,
fileLabel: selectedFile?.path ?? '',
newWidgetId: 'files-new-comment-input',
mapDraftToRange: (draft) => ({ start: draft.startLine, end: draft.endLine }),
onSave: handleSaveComment,
onCancel: () => {
setLineSelection(null);
cancel();
},
onEdit: (draft) => {
startEdit(draft);
setLineSelection({ start: draft.startLine, end: draft.endLine });
},
onDelete: deleteDraft,
});
}, [cancel, commentText, deleteDraft, editingDraftId, filesFileDrafts, handleSaveComment, isDragging, lineSelection, selectedFile?.path, startEdit]);
const renderShikiFileView = React.useCallback((file: FileNode, content: string) => {
return (
);
}, [currentTheme.metadata.variant, pierreTheme, wrapLines]);
const renderFloatingFileControls = ({ exitFullscreenOnly = false }: { exitFullscreenOnly?: boolean } = {}) => {
if (!selectedFile) {
return null;
}
const withTooltip = (label: React.ReactNode, trigger: React.ReactElement) => (
{trigger}
{label}
);
return (
{canEdit && textViewMode === 'edit' && (
<>
{isSaving ? (
{t('filesView.editor.saving')}
) : autoSaveEnabled && autoSaveStatus === 'saved' && !isDirty ? (
{t('filesView.editor.saved')}
) : isDirty ? withTooltip(t(autoSaveEnabled ? 'filesView.editor.saveNowTitle' : 'filesView.editor.saveNowManualTitle', { shortcut: `${getModifierLabel()}+S` }),
) : null}
{withTooltip(autoSaveEnabled ? t('filesView.editor.autoSaveOn') : t('filesView.editor.manualSave'),
)}
>
)}
{t('filesView.editor.openInDesktopApp')}
{openInApps.map((app) => (
void handleOpenInApp(app)}
>
{app.label}
))}
{openInCacheStale ? (
void loadOpenInApps(true)}
>
{t('filesView.editor.refreshApps')}
) : null}
{!isSelectedImage && (
<>
{withTooltip(wrapLines ? t('filesView.editor.disableLineWrap') : t('filesView.editor.enableLineWrap'),
)}
{textViewMode === 'edit' && (
<>
{withTooltip(t('filesView.editor.findInFile'),
)}
{withTooltip(t('filesView.editor.goToLine'),
)}
>
)}
>
)}
{canUseShikiFileView && canEdit && !isJson && !isHtml && (
{
saveTextViewMode(textViewMode === 'view' ? 'edit' : 'view');
}}
/>
)}
{(isMarkdown || isHtmlFile(selectedFile?.path ?? '')) && (
{
if (isHtmlFile(selectedFile?.path ?? '')) {
saveHtmlViewMode(getHtmlViewMode() === 'preview' ? 'edit' : 'preview');
} else {
saveMdViewMode(getMdViewMode() === 'preview' ? 'edit' : 'preview');
}
}}
/>
)}
{isMarkdown && getMdViewMode() === 'preview' && showMessageTTSButtons && (
{isTTSPlaying ? t('filesView.tts.stopSpeaking') : t('filesView.tts.readAloud')}
)}
{isDrawio && (
<>
saveDrawioViewMode(drawioViewMode === 'preview' ? 'edit' : 'preview')}
/>
{drawioViewMode === 'preview' && (
)}
>
)}
{isJson && (
withTooltip(jsonViewMode === 'tree' ? t('filesView.editor.switchToTextView') : t('filesView.editor.switchToTreeView'),
)
)}
{canCopy && (
withTooltip(t('filesView.editor.copyFileContents'),
)
)}
{canCopyPath && (
withTooltip(t('filesView.editor.copyFilePathTitle', { path: displaySelectedPath }),
)
)}
{files.downloadFile && (
withTooltip(t('filesView.editor.saveFile'),
)
)}
{exitFullscreenOnly ? (
withTooltip(t('filesView.editor.exitFullscreen'),
)
) : (!isMobile && mode === 'full' && (
withTooltip(isFullscreen ? t('filesView.editor.exitFullscreen') : t('filesView.editor.fullscreen'),
)
))}
);
};
const fileViewer = (
{/* Row 1: Tabs */}
{showEditorTabsRow ? (
{isMobile && showMobilePageContent && (
)}
{isMobile ? (
selectedFile ? (
{openFiles.map((file) => {
const isActive = selectedFile?.path === file.path;
return (
{
const target = event.target as HTMLElement;
if (target.closest('[data-close-open-file]')) {
event.preventDefault();
return;
}
if (!isActive) {
void handleSelectFile(file);
}
}}
className={cn(
'flex min-w-0 items-center justify-between gap-2 overflow-hidden',
isActive && 'bg-[var(--interactive-selection)] text-[var(--interactive-selection-foreground)]'
)}
>
);
})}
) : (
{t('filesView.editor.selectFile')}
)
) : (
openFiles.length > 0 ? (
{editorTabsOverflow.left && (
)}
{editorTabsOverflow.right && (
)}
{openFiles.map((file) => {
const isActive = selectedFile?.path === file.path;
return (
);
})}
) : (
{t('filesView.editor.selectFile')}
)
)}
) : null}
{selectedFile && !isSearchOpen && (
setIsFloatingToolbarOpen(true)}
onMouseLeave={() => {
if (toolbarDropdownOpenCountRef.current > 0) return;
setIsFloatingToolbarOpen(false);
}}
>
{isFloatingToolbarOpen ? (
renderFloatingFileControls()
) : (
{t('filesView.editor.controlsTitle')}
)}
)}
{!selectedFile ? (
{t('filesView.editor.pickFileFromTree')}
) : (fileLoading || isImageAssetAuthLoading) ? (
suppressFileLoadingIndicator
?
: (
{t('filesView.state.loading')}
)
) : fileError ? (
{fileError}
) : isSelectedImage ? (
) : selectedFile && isDrawio && drawioViewMode === 'preview' ? (
) : selectedFile && isJson && jsonViewMode === 'tree' ? (
{t('filesView.error.jsonViewerUnavailable')}
{t('filesView.error.switchToTextMode')}
}
>
) : selectedFile && isMarkdown && getMdViewMode() === 'preview' ? (
{fileContent.length > 500 * 1024 && (
{t('filesView.warning.largeFilePreviewLimited', { sizeKb: Math.round(fileContent.length / 1024) })}
)}
{t('filesView.error.previewUnavailable')}
{t('filesView.error.switchToEditMode')}
}
>
) : selectedFile && isHtml && htmlViewMode === 'preview' ? (
) : selectedFile && canUseShikiFileView && textViewMode === 'view' ? (
renderShikiFileView(selectedFile, draftContent)
) : (
{
editorViewRef.current = view;
setEditorViewReadyNonce((value) => value + 1);
window.requestAnimationFrame(() => {
nudgeEditorSelectionAboveKeyboard(view);
});
}}
onViewDestroy={() => {
if (editorViewRef.current) {
editorViewRef.current = null;
}
setEditorViewReadyNonce((value) => value + 1);
}}
enableSearch
searchOpen={isSearchOpen}
onSearchOpenChange={setIsSearchOpen}
highlightLines={lineSelection
? {
start: Math.min(lineSelection.start, lineSelection.end),
end: Math.max(lineSelection.start, lineSelection.end),
}
: undefined}
lineNumbersConfig={{
domEventHandlers: {
mousedown: (view: EditorView, line: { from: number; to: number }, event: Event) => {
if (!(event instanceof MouseEvent)) {
return false;
}
if (event.button !== 0) {
return false;
}
event.preventDefault();
const lineNumber = view.state.doc.lineAt(line.from).number;
// Mobile: tap-to-extend selection
if (isMobile && lineSelection && !event.shiftKey) {
const start = Math.min(lineSelection.start, lineSelection.end, lineNumber);
const end = Math.max(lineSelection.start, lineSelection.end, lineNumber);
setLineSelection({ start, end });
isSelectingRef.current = false;
selectionStartRef.current = null;
setIsDragging(false);
return true;
}
isSelectingRef.current = true;
selectionStartRef.current = lineNumber;
setIsDragging(true);
if (lineSelection && event.shiftKey) {
const start = Math.min(lineSelection.start, lineNumber);
const end = Math.max(lineSelection.end, lineNumber);
setLineSelection({ start, end });
} else {
setLineSelection({ start: lineNumber, end: lineNumber });
}
return true;
},
mouseover: (view: EditorView, line: { from: number; to: number }, event: Event) => {
if (!(event instanceof MouseEvent)) {
return false;
}
if (event.buttons !== 1) {
return false;
}
if (!isSelectingRef.current || selectionStartRef.current === null) {
return false;
}
const lineNumber = view.state.doc.lineAt(line.from).number;
const start = Math.min(selectionStartRef.current, lineNumber);
const end = Math.max(selectionStartRef.current, lineNumber);
setLineSelection({ start, end });
setIsDragging(true);
return false;
},
mouseup: () => {
isSelectingRef.current = false;
selectionStartRef.current = null;
setIsDragging(false);
return false;
},
},
}}
/>
{shouldMaskEditorForPendingNavigation && (
{t('filesView.state.openingFileAtChange')}
)}
)}
);
const hasTree = Boolean(root && childrenByDir[root]);
const rootLoadError = root ? loadErrorsByDir[root] : null;
const treePanel = (
{searching ? (
-
{t('filesView.tree.search.searching')}
) : searchResults.length > 0 ? (
searchResults.map((node) => {
const isActive = selectedFile?.path === node.path;
return (
-
);
})
) : rootLoadError ? (
-
{rootLoadError}
) : hasTree ? (
renderTree(root, 0)
) : (
- {t('filesView.state.loading')}
)}
);
// Fullscreen file viewer overlay
const fullscreenViewer = mode === 'full' && isFullscreen && selectedFile && (
{/* Fullscreen content */}
{renderFloatingFileControls({ exitFullscreenOnly: true })}
{(fileLoading || isImageAssetAuthLoading) ? (
suppressFileLoadingIndicator
?
: (
Loading…
)
) : fileError ? (
{fileError}
) : isSelectedImage ? (
) : isMarkdown && getMdViewMode() === 'preview' ? (
{fileContent.length > 500 * 1024 && (
{t('filesView.warning.largeFilePreviewLimited', { sizeKb: Math.round(fileContent.length / 1024) })}
)}
{t('filesView.error.previewUnavailable')}
{t('filesView.error.switchToEditMode')}
}
>
) : canUseShikiFileView && textViewMode === 'view' ? (
renderShikiFileView(selectedFile, draftContent)
) : (
{
editorViewRef.current = view;
window.requestAnimationFrame(() => {
nudgeEditorSelectionAboveKeyboard(view);
});
}}
onViewDestroy={() => {
if (editorViewRef.current) {
editorViewRef.current = null;
}
}}
/>
{shouldMaskEditorForPendingNavigation && (
{t('filesView.state.openingFileAtChange')}
)}
)}
);
return (
{fullscreenViewer}
{isMobile ? (
showMobilePageContent ? (
fileViewer
) : (
treePanel
)
) : mode === 'editor-only' ? (
) : (
{screenWidth >= 700 && (
{treePanel}
)}
{fileViewer}
)}
);
};