From 360660b850016cb0edc7f8635ecc9bb2f58890ce Mon Sep 17 00:00:00 2001 From: seb Date: Thu, 15 Oct 2026 01:45:53 +0200 Subject: [PATCH] u --- src/components/manifest/QuoteList.jsx | 7 +- src/components/vfs/VfsTextEditor.jsx | 431 ++++++++++++++++---------- 2 files changed, 276 insertions(+), 162 deletions(-) diff --git a/src/components/manifest/QuoteList.jsx b/src/components/manifest/QuoteList.jsx index 1caa075..525acde 100644 --- a/src/components/manifest/QuoteList.jsx +++ b/src/components/manifest/QuoteList.jsx @@ -27,7 +27,6 @@ import { formatDate, loadParties, manifestConfig, - numberDisplay, partyDisplayName, } from './manifestConfig'; @@ -90,7 +89,6 @@ class QuoteRow extends PureComponent { - @@ -125,9 +123,6 @@ class QuoteRow extends PureComponent { {quote.number} - - {numberDisplay(quote.externalNumber)} - @@ -593,7 +588,7 @@ export class QuoteList extends PureComponent { userSelect: 'none', }} > - Nr. / Ref. + Nr. Datum Kunde Pos. diff --git a/src/components/vfs/VfsTextEditor.jsx b/src/components/vfs/VfsTextEditor.jsx index b03a561..2c76b1d 100644 --- a/src/components/vfs/VfsTextEditor.jsx +++ b/src/components/vfs/VfsTextEditor.jsx @@ -5,22 +5,124 @@ import IconButton from '@mui/material/IconButton'; import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; import Chip from '@mui/material/Chip'; +import Tooltip from '@mui/material/Tooltip'; +import Alert from '@mui/material/Alert'; + import SaveRoundedIcon from '@mui/icons-material/SaveRounded'; import UndoRoundedIcon from '@mui/icons-material/UndoRounded'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded'; import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded'; -import Alert from '@mui/material/Alert'; +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'; +/** + * Returns CodeMirror language extension and human-readable language label + * based on fileName extension and MIME type. + */ +function getLanguageInfo(fileName, mime) { + if (!fileName && !mime) return { extension: null, label: 'Plain Text' }; + const ext = fileName ? fileName.split('.').pop()?.toLowerCase() : ''; + + switch (ext) { + case 'js': + case 'jsx': + case 'mjs': + case 'cjs': + return { extension: javascript({ jsx: true }), label: 'JavaScript' }; + case 'ts': + case 'tsx': + return { extension: javascript({ jsx: true, typescript: true }), label: 'TypeScript' }; + case 'json': + case 'json5': + return { extension: json(), label: 'JSON' }; + case 'html': + case 'htm': + return { extension: html(), label: 'HTML' }; + case 'css': + case 'scss': + case 'less': + return { extension: css(), label: 'CSS' }; + case 'md': + case 'markdown': + return { extension: markdown(), label: 'Markdown' }; + case 'sql': + return { extension: sql(), label: 'SQL' }; + case 'xml': + case 'svg': + return { extension: xml(), label: 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' }; + } +} + +// 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) { super(props); - this.textareaRef = React.createRef(); - this.gutterRef = React.createRef(); + this.editorView = null; this.state = { wrapLines: true, cursorLine: 1, @@ -38,57 +140,47 @@ export default class VfsTextEditor extends PureComponent { } } - focusEditor = () => { - setTimeout(() => { - this.textareaRef.current?.focus(); - }, 50); + handleEditorCreate = (view) => { + this.editorView = view; + this.focusEditor(); }; - handleScroll = () => { - if (this.textareaRef.current && this.gutterRef.current) { - this.gutterRef.current.scrollTop = this.textareaRef.current.scrollTop; - } + focusEditor = () => { + setTimeout(() => { + this.editorView?.focus(); + }, 50); }; toggleWrap = () => { this.setState((prev) => ({ wrapLines: !prev.wrapLines })); }; - handleCursorMove = () => { - const ta = this.textareaRef.current; - if (!ta) return; - const pos = ta.selectionStart || 0; - const textBefore = (this.props.content || '').substring(0, pos); - const lines = textBefore.split('\n'); - const line = lines.length; - const col = lines[lines.length - 1].length + 1; - if (this.state.cursorLine !== line || this.state.cursorCol !== col) { - this.setState({ cursorLine: line, cursorCol: col }); + handleViewUpdate = (viewUpdate) => { + if (viewUpdate.selectionSet || viewUpdate.docChanged) { + const state = viewUpdate.state; + const head = state.selection.main.head; + const line = state.doc.lineAt(head); + const cursorLine = line.number; + const cursorCol = head - line.from + 1; + if (this.state.cursorLine !== cursorLine || this.state.cursorCol !== cursorCol) { + this.setState({ cursorLine, cursorCol }); + } } }; handleMentionInChat = () => { - const ta = this.textareaRef.current; + const view = this.editorView; let lineInfo = null; - if (ta) { - const start = ta.selectionStart; - const end = ta.selectionEnd; - if (typeof start === 'number' && typeof end === 'number' && end > start) { - const content = this.props.content || ''; - const beforeStart = content.substring(0, start); - const startLine = beforeStart.split('\n').length; - - let effectiveEnd = end; - if (effectiveEnd > start && content[effectiveEnd - 1] === '\n') { - effectiveEnd -= 1; - } - const beforeEnd = content.substring(0, effectiveEnd); - const endLine = Math.max(startLine, beforeEnd.split('\n').length); - + if (view) { + const { from, to } = view.state.selection.main; + if (typeof from === 'number' && typeof to === 'number' && to > from) { + const startLine = view.state.doc.lineAt(from).number; + const endLine = view.state.doc.lineAt(to).number; + const selectedText = view.state.sliceDoc(from, to); lineInfo = { startLine, endLine, - selectedText: content.substring(start, end), + selectedText, }; } } @@ -98,49 +190,17 @@ export default class VfsTextEditor extends PureComponent { } }; - handleKeyDown = (e) => { - // Ctrl+S or Cmd+S -> Save - if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') { - e.preventDefault(); - if (this.props.isDirty && !this.props.isSaving) { - this.props.onSave(); - } - return; - } - - // Tab key -> Insert 2 spaces - if (e.key === 'Tab') { - e.preventDefault(); - const ta = this.textareaRef.current; - if (!ta) return; - const start = ta.selectionStart; - const end = ta.selectionEnd; - const val = this.props.content; - const next = val.substring(0, start) + ' ' + val.substring(end); - this.props.onChange(next); - setTimeout(() => { - ta.selectionStart = ta.selectionEnd = start + 2; - }, 0); - } - }; - handleRevert = () => { if (window.confirm('Möchtest du alle ungespeicherten Änderungen verwerfen?')) { this.props.onChange(this.props.originalContent); } }; - renderLineNumbers(lineCount) { - const lines = []; - for (let i = 1; i <= lineCount; i++) { - lines.push( -
- {i} -
- ); + handleSaveShortcut = () => { + if (this.props.isDirty && !this.props.isSaving) { + this.props.onSave?.(); } - return lines; - } + }; render() { const { @@ -151,6 +211,7 @@ export default class VfsTextEditor extends PureComponent { isSaving, saveError, loadingContent, + onChange, onSave, onClose, onMentionInChat, @@ -158,8 +219,36 @@ export default class VfsTextEditor extends PureComponent { onShowRunner, } = this.props; - const lineCount = (content || '').split('\n').length; - const charCount = (content || '').length; + const { wrapLines, cursorLine, cursorCol } = this.state; + const text = content || ''; + 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); + } return ( - {/* Editor Top Bar */} + {/* Editor Top Bar (Standard GUI Theme) */} {/* File Name & Status */} - + @@ -232,16 +326,43 @@ export default class VfsTextEditor extends PureComponent { Exe )} - {onMentionInChat && ( - - + + + + + + + {onMentionInChat && ( + + + + + )} {isDirty && ( - - - + + + + + )}