This commit is contained in:
seb
2026-10-17 01:58:30 +02:00
parent bb22da7aae
commit e031458a42
3 changed files with 243 additions and 133 deletions

View File

@@ -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 (
<CodeMirror
value={value}
height="100%"
style={{ height: '100%', width: '100%' }}
theme={oneDark}
extensions={extensions}
basicSetup={{
lineNumbers: true,
highlightActiveLineGutter: true,
highlightSpecialChars: true,
history: true,
foldGutter: true,
dropCursor: true,
allowMultipleSelections: true,
indentOnInput: true,
syntaxHighlighting: true,
bracketMatching: true,
closeBrackets: true,
autocompletion: true,
rectangularSelection: true,
crosshairCursor: false,
highlightActiveLine: true,
highlightSelectionMatches: true,
closeBracketsKeymap: true,
defaultKeymap: true,
searchKeymap: true,
historyKeymap: true,
foldKeymap: true,
completionKeymap: true,
lintKeymap: true,
}}
readOnly={readOnly}
editable={!readOnly}
onCreateEditor={onCreateEditor}
onUpdate={onUpdate}
onChange={onChange}
/>
);
}
}

View File

@@ -16,26 +16,16 @@ import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineR
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded'; import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
import WrapTextRoundedIcon from '@mui/icons-material/WrapTextRounded'; 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 VfsFileIcon from './VfsFileIcon';
import { formatVfsSize } from './vfsUtils'; import { formatVfsSize } from './vfsUtils';
const CodeMirrorCore = React.lazy(() => import('./CodeMirrorCore'));
/** /**
* Returns CodeMirror language extension and human-readable language label * Returns human-readable language label based on fileName extension and MIME type.
* based on fileName extension and MIME type.
*/ */
function getLanguageInfo(fileName, mime) { function getLanguageLabel(fileName, mime) {
if (!fileName && !mime) return { extension: null, label: 'Plain Text' }; if (!fileName && !mime) return 'Plain Text';
const ext = fileName ? fileName.split('.').pop()?.toLowerCase() : ''; const ext = fileName ? fileName.split('.').pop()?.toLowerCase() : '';
switch (ext) { switch (ext) {
@@ -43,81 +33,39 @@ function getLanguageInfo(fileName, mime) {
case 'jsx': case 'jsx':
case 'mjs': case 'mjs':
case 'cjs': case 'cjs':
return { extension: javascript({ jsx: true }), label: 'JavaScript' }; return 'JavaScript';
case 'ts': case 'ts':
case 'tsx': case 'tsx':
return { extension: javascript({ jsx: true, typescript: true }), label: 'TypeScript' }; return 'TypeScript';
case 'json': case 'json':
case 'json5': case 'json5':
return { extension: json(), label: 'JSON' }; return 'JSON';
case 'html': case 'html':
case 'htm': case 'htm':
return { extension: html(), label: 'HTML' }; return 'HTML';
case 'css': case 'css':
case 'scss': case 'scss':
case 'less': case 'less':
return { extension: css(), label: 'CSS' }; return 'CSS';
case 'md': case 'md':
case 'markdown': case 'markdown':
return { extension: markdown(), label: 'Markdown' }; return 'Markdown';
case 'sql': case 'sql':
return { extension: sql(), label: 'SQL' }; return 'SQL';
case 'xml': case 'xml':
case 'svg': case 'svg':
return { extension: xml(), label: ext === 'svg' ? 'SVG' : 'XML' }; return ext === 'svg' ? 'SVG' : 'XML';
default: default:
if (mime?.includes('json')) return { extension: json(), label: 'JSON' }; if (mime?.includes('json')) return 'JSON';
if (mime?.includes('javascript')) return { extension: javascript({ jsx: true }), label: 'JavaScript' }; if (mime?.includes('javascript')) return 'JavaScript';
if (mime?.includes('html')) return { extension: html(), label: 'HTML' }; if (mime?.includes('html')) return 'HTML';
if (mime?.includes('xml') || mime?.includes('svg')) return { extension: xml(), label: 'XML' }; if (mime?.includes('xml') || mime?.includes('svg')) return 'XML';
if (mime?.includes('markdown')) return { extension: markdown(), label: 'Markdown' }; if (mime?.includes('markdown')) return 'Markdown';
return { extension: null, label: 'Plain Text' }; return 'Plain Text';
} }
} }
// Softer dark theme for the editor code area (One Dark slate #282c34 / #21252b)
const EDITOR_BG = '#282c34'; 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 { export default class VfsTextEditor extends PureComponent {
constructor(props) { constructor(props) {
@@ -224,31 +172,7 @@ export default class VfsTextEditor extends PureComponent {
const lineCount = text.split('\n').length; const lineCount = text.split('\n').length;
const charCount = text.length; const charCount = text.length;
const { extension: langExt, label: langLabel } = getLanguageInfo(fileName, file?.mime); const langLabel = getLanguageLabel(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);
}
return ( return (
<Box <Box
@@ -441,42 +365,39 @@ export default class VfsTextEditor extends PureComponent {
}, },
}} }}
> >
<CodeMirror <React.Suspense
value={text} fallback={
height="100%" <Box
style={{ height: '100%', width: '100%' }} sx={{
theme={oneDark} height: '100%',
extensions={extensions} width: '100%',
basicSetup={{ display: 'flex',
lineNumbers: true, flexDirection: 'column',
highlightActiveLineGutter: true, alignItems: 'center',
highlightSpecialChars: true, justifyContent: 'center',
history: true, gap: 1.5,
foldGutter: true, backgroundColor: EDITOR_BG,
drawSelection: true, }}
dropCursor: true, >
allowMultipleSelections: true, <CircularProgress size={28} sx={{ color: '#61afef' }} />
indentOnInput: true, <Typography variant="caption" sx={{ color: '#abb2bf' }}>
syntaxHighlighting: true, Editor wird geladen…
bracketMatching: true, </Typography>
closeBrackets: true, </Box>
autocompletion: true, }
rectangularSelection: true, >
crosshairCursor: false, <CodeMirrorCore
highlightActiveLine: true, value={text}
highlightSelectionMatches: true, fileName={fileName}
closeBracketsKeymap: true, mime={file?.mime}
defaultKeymap: true, wrapLines={wrapLines}
searchKeymap: true, readOnly={this.props.readOnly}
historyKeymap: true, onSaveShortcut={this.handleSaveShortcut}
foldKeymap: true, onCreateEditor={this.handleEditorCreate}
completionKeymap: true, onUpdate={this.handleViewUpdate}
lintKeymap: true, onChange={onChange}
}} />
onCreateEditor={this.handleEditorCreate} </React.Suspense>
onUpdate={this.handleViewUpdate}
onChange={onChange}
/>
</Box> </Box>
)} )}
</Box> </Box>

View File

@@ -361,6 +361,11 @@ export default defineConfig(({ command }) => ({
name: 'jsquash-image', name: 'jsquash-image',
test: /node_modules\/@jsquash\/(avif|resize)\//, 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, priority: 110,
name: 'ag-grid-vendor', name: 'ag-grid-vendor',
@@ -379,7 +384,7 @@ export default defineConfig(({ command }) => ({
modulePreload: { modulePreload: {
resolveDependencies(filename, deps, { hostType }) { resolveDependencies(filename, deps, { hostType }) {
if (hostType === 'html') { 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; return deps;
}, },