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 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 (
<Box
@@ -441,42 +365,39 @@ export default class VfsTextEditor extends PureComponent {
},
}}
>
<CodeMirror
value={text}
height="100%"
style={{ height: '100%', width: '100%' }}
theme={oneDark}
extensions={extensions}
basicSetup={{
lineNumbers: true,
highlightActiveLineGutter: true,
highlightSpecialChars: true,
history: true,
foldGutter: true,
drawSelection: 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,
}}
onCreateEditor={this.handleEditorCreate}
onUpdate={this.handleViewUpdate}
onChange={onChange}
/>
<React.Suspense
fallback={
<Box
sx={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 1.5,
backgroundColor: EDITOR_BG,
}}
>
<CircularProgress size={28} sx={{ color: '#61afef' }} />
<Typography variant="caption" sx={{ color: '#abb2bf' }}>
Editor wird geladen…
</Typography>
</Box>
}
>
<CodeMirrorCore
value={text}
fileName={fileName}
mime={file?.mime}
wrapLines={wrapLines}
readOnly={this.props.readOnly}
onSaveShortcut={this.handleSaveShortcut}
onCreateEditor={this.handleEditorCreate}
onUpdate={this.handleViewUpdate}
onChange={onChange}
/>
</React.Suspense>
</Box>
)}
</Box>

View File

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