diff --git a/src/components/vfs/CodeMirrorCore.jsx b/src/components/vfs/CodeMirrorCore.jsx
new file mode 100644
index 0000000..fc6087e
--- /dev/null
+++ b/src/components/vfs/CodeMirrorCore.jsx
@@ -0,0 +1,184 @@
+import React, { PureComponent } from 'react';
+import CodeMirror, { EditorView, keymap, scrollPastEnd } from '@uiw/react-codemirror';
+import { indentWithTab } from '@codemirror/commands';
+import { oneDark } from '@codemirror/theme-one-dark';
+import { javascript } from '@codemirror/lang-javascript';
+import { json } from '@codemirror/lang-json';
+import { html } from '@codemirror/lang-html';
+import { css } from '@codemirror/lang-css';
+import { markdown } from '@codemirror/lang-markdown';
+import { sql } from '@codemirror/lang-sql';
+import { xml } from '@codemirror/lang-xml';
+
+function getLanguageExtension(fileName, mime) {
+ if (!fileName && !mime) return null;
+ const ext = fileName ? fileName.split('.').pop()?.toLowerCase() : '';
+
+ switch (ext) {
+ case 'js':
+ case 'jsx':
+ case 'mjs':
+ case 'cjs':
+ return javascript({ jsx: true });
+ case 'ts':
+ case 'tsx':
+ return javascript({ jsx: true, typescript: true });
+ case 'json':
+ case 'json5':
+ return json();
+ case 'html':
+ case 'htm':
+ return html();
+ case 'css':
+ case 'scss':
+ case 'less':
+ return css();
+ case 'md':
+ case 'markdown':
+ return markdown();
+ case 'sql':
+ return sql();
+ case 'xml':
+ case 'svg':
+ return xml();
+ default:
+ if (mime?.includes('json')) return json();
+ if (mime?.includes('javascript')) return javascript({ jsx: true });
+ if (mime?.includes('html')) return html();
+ if (mime?.includes('xml') || mime?.includes('svg')) return xml();
+ if (mime?.includes('markdown')) return markdown();
+ return null;
+ }
+}
+
+// Softer dark theme for the editor code area (One Dark slate #282c34 / #21252b)
+const EDITOR_BG = '#282c34';
+const GUTTER_BG = '#21252b';
+
+const customEditorTheme = EditorView.theme({
+ '&': {
+ height: '100%',
+ maxHeight: '100%',
+ fontSize: '13px',
+ fontFamily: 'Consolas, Monaco, "Courier New", monospace',
+ backgroundColor: `${EDITOR_BG} !important`,
+ color: '#abb2bf',
+ },
+ '.cm-scroller': {
+ overflow: 'auto !important',
+ height: '100% !important',
+ maxHeight: '100%',
+ fontFamily: 'inherit',
+ lineHeight: '21px',
+ },
+ '.cm-content': {
+ padding: '10px 0',
+ },
+ '.cm-gutters': {
+ backgroundColor: `${GUTTER_BG} !important`,
+ color: '#636d83 !important',
+ borderRight: '1px solid rgba(255, 255, 255, 0.08) !important',
+ },
+ '.cm-lineNumbers .cm-gutterElement': {
+ padding: '0 10px 0 16px',
+ minWidth: '40px',
+ },
+ '.cm-activeLine': {
+ backgroundColor: 'rgba(255, 255, 255, 0.04) !important',
+ },
+ '.cm-activeLineGutter': {
+ backgroundColor: 'rgba(255, 255, 255, 0.07) !important',
+ color: '#abb2bf !important',
+ },
+ '&.cm-focused': {
+ outline: 'none',
+ },
+});
+
+export default class CodeMirrorCore extends PureComponent {
+ buildExtensions() {
+ const { fileName, mime, wrapLines, readOnly, onSaveShortcut } = this.props;
+ const langExt = getLanguageExtension(fileName, mime);
+
+ const keys = [indentWithTab];
+ if (onSaveShortcut) {
+ keys.push({
+ key: 'Mod-s',
+ run: () => {
+ onSaveShortcut();
+ return true;
+ },
+ });
+ }
+
+ const extensions = [
+ customEditorTheme,
+ scrollPastEnd(),
+ keymap.of(keys),
+ ];
+
+ if (langExt) {
+ extensions.push(langExt);
+ }
+ if (wrapLines) {
+ extensions.push(EditorView.lineWrapping);
+ }
+ if (readOnly) {
+ extensions.push(EditorView.editable.of(false));
+ }
+
+ return extensions;
+ }
+
+ render() {
+ const {
+ value,
+ readOnly,
+ onCreateEditor,
+ onUpdate,
+ onChange,
+ } = this.props;
+
+ const extensions = this.buildExtensions();
+
+ return (
+
+ );
+ }
+}
diff --git a/src/components/vfs/VfsTextEditor.jsx b/src/components/vfs/VfsTextEditor.jsx
index 2c76b1d..e36ddef 100644
--- a/src/components/vfs/VfsTextEditor.jsx
+++ b/src/components/vfs/VfsTextEditor.jsx
@@ -16,26 +16,16 @@ import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineR
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
import WrapTextRoundedIcon from '@mui/icons-material/WrapTextRounded';
-import CodeMirror, { EditorView, keymap, scrollPastEnd } from '@uiw/react-codemirror';
-import { indentWithTab } from '@codemirror/commands';
-import { oneDark } from '@codemirror/theme-one-dark';
-import { javascript } from '@codemirror/lang-javascript';
-import { json } from '@codemirror/lang-json';
-import { html } from '@codemirror/lang-html';
-import { css } from '@codemirror/lang-css';
-import { markdown } from '@codemirror/lang-markdown';
-import { sql } from '@codemirror/lang-sql';
-import { xml } from '@codemirror/lang-xml';
-
import VfsFileIcon from './VfsFileIcon';
import { formatVfsSize } from './vfsUtils';
+const CodeMirrorCore = React.lazy(() => import('./CodeMirrorCore'));
+
/**
- * Returns CodeMirror language extension and human-readable language label
- * based on fileName extension and MIME type.
+ * Returns human-readable language label based on fileName extension and MIME type.
*/
-function getLanguageInfo(fileName, mime) {
- if (!fileName && !mime) return { extension: null, label: 'Plain Text' };
+function getLanguageLabel(fileName, mime) {
+ if (!fileName && !mime) return 'Plain Text';
const ext = fileName ? fileName.split('.').pop()?.toLowerCase() : '';
switch (ext) {
@@ -43,81 +33,39 @@ function getLanguageInfo(fileName, mime) {
case 'jsx':
case 'mjs':
case 'cjs':
- return { extension: javascript({ jsx: true }), label: 'JavaScript' };
+ return 'JavaScript';
case 'ts':
case 'tsx':
- return { extension: javascript({ jsx: true, typescript: true }), label: 'TypeScript' };
+ return 'TypeScript';
case 'json':
case 'json5':
- return { extension: json(), label: 'JSON' };
+ return 'JSON';
case 'html':
case 'htm':
- return { extension: html(), label: 'HTML' };
+ return 'HTML';
case 'css':
case 'scss':
case 'less':
- return { extension: css(), label: 'CSS' };
+ return 'CSS';
case 'md':
case 'markdown':
- return { extension: markdown(), label: 'Markdown' };
+ return 'Markdown';
case 'sql':
- return { extension: sql(), label: 'SQL' };
+ return 'SQL';
case 'xml':
case 'svg':
- return { extension: xml(), label: ext === 'svg' ? 'SVG' : 'XML' };
+ return ext === 'svg' ? 'SVG' : 'XML';
default:
- if (mime?.includes('json')) return { extension: json(), label: 'JSON' };
- if (mime?.includes('javascript')) return { extension: javascript({ jsx: true }), label: 'JavaScript' };
- if (mime?.includes('html')) return { extension: html(), label: 'HTML' };
- if (mime?.includes('xml') || mime?.includes('svg')) return { extension: xml(), label: 'XML' };
- if (mime?.includes('markdown')) return { extension: markdown(), label: 'Markdown' };
- return { extension: null, label: 'Plain Text' };
+ if (mime?.includes('json')) return 'JSON';
+ if (mime?.includes('javascript')) return 'JavaScript';
+ if (mime?.includes('html')) return 'HTML';
+ if (mime?.includes('xml') || mime?.includes('svg')) return 'XML';
+ if (mime?.includes('markdown')) return 'Markdown';
+ return 'Plain Text';
}
}
-// Softer dark theme for the editor code area (One Dark slate #282c34 / #21252b)
const EDITOR_BG = '#282c34';
-const GUTTER_BG = '#21252b';
-
-const customEditorTheme = EditorView.theme({
- '&': {
- height: '100%',
- maxHeight: '100%',
- fontSize: '13px',
- fontFamily: 'Consolas, Monaco, "Courier New", monospace',
- backgroundColor: `${EDITOR_BG} !important`,
- color: '#abb2bf',
- },
- '.cm-scroller': {
- overflow: 'auto !important',
- height: '100% !important',
- maxHeight: '100%',
- fontFamily: 'inherit',
- lineHeight: '21px',
- },
- '.cm-content': {
- padding: '10px 0',
- },
- '.cm-gutters': {
- backgroundColor: `${GUTTER_BG} !important`,
- color: '#636d83 !important',
- borderRight: '1px solid rgba(255, 255, 255, 0.08) !important',
- },
- '.cm-lineNumbers .cm-gutterElement': {
- padding: '0 10px 0 16px',
- minWidth: '40px',
- },
- '.cm-activeLine': {
- backgroundColor: 'rgba(255, 255, 255, 0.04) !important',
- },
- '.cm-activeLineGutter': {
- backgroundColor: 'rgba(255, 255, 255, 0.07) !important',
- color: '#abb2bf !important',
- },
- '&.cm-focused': {
- outline: 'none',
- },
-});
export default class VfsTextEditor extends PureComponent {
constructor(props) {
@@ -224,31 +172,7 @@ export default class VfsTextEditor extends PureComponent {
const lineCount = text.split('\n').length;
const charCount = text.length;
- const { extension: langExt, label: langLabel } = getLanguageInfo(fileName, file?.mime);
-
- // Build CodeMirror extensions list
- const extensions = [
- customEditorTheme,
- scrollPastEnd(),
- keymap.of([
- indentWithTab,
- {
- key: 'Mod-s',
- run: () => {
- this.handleSaveShortcut();
- return true;
- },
- },
- ]),
- ];
-
- if (langExt) {
- extensions.push(langExt);
- }
-
- if (wrapLines) {
- extensions.push(EditorView.lineWrapping);
- }
+ const langLabel = getLanguageLabel(fileName, file?.mime);
return (
-
+
+
+
+ Editor wird geladen…
+
+
+ }
+ >
+
+
)}
diff --git a/vite.config.js b/vite.config.js
index 2789fab..19b6bd2 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -361,6 +361,11 @@ export default defineConfig(({ command }) => ({
name: 'jsquash-image',
test: /node_modules\/@jsquash\/(avif|resize)\//,
},
+ {
+ priority: 120,
+ name: 'codemirror',
+ test: /(?:node_modules\/(?:@codemirror|@uiw|@lezer|crelt|style-mod|w3c-keyname|@marijn\/find-cluster-break)|src\/components\/vfs\/CodeMirrorCore)/,
+ },
{
priority: 110,
name: 'ag-grid-vendor',
@@ -379,7 +384,7 @@ export default defineConfig(({ command }) => ({
modulePreload: {
resolveDependencies(filename, deps, { hostType }) {
if (hostType === 'html') {
- return deps.filter((dep) => !dep.includes('ag-grid') && !dep.includes('AutomatGrid'));
+ return deps.filter((dep) => !dep.includes('ag-grid') && !dep.includes('AutomatGrid') && !dep.includes('codemirror'));
}
return deps;
},