u
This commit is contained in:
184
src/components/vfs/CodeMirrorCore.jsx
Normal file
184
src/components/vfs/CodeMirrorCore.jsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user