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; },