146 lines
5.2 KiB
TypeScript
146 lines
5.2 KiB
TypeScript
import { useRef, useState } from 'react';
|
|
import CodeMirror, { EditorView } from '@uiw/react-codemirror';
|
|
import { json } from '@codemirror/lang-json';
|
|
import { xml } from '@codemirror/lang-xml';
|
|
import { javascript } from '@codemirror/lang-javascript';
|
|
import { python } from '@codemirror/lang-python';
|
|
import { cpp } from '@codemirror/lang-cpp';
|
|
import { css } from '@codemirror/lang-css';
|
|
import { rust } from '@codemirror/lang-rust';
|
|
import { php } from '@codemirror/lang-php';
|
|
import { sql } from '@codemirror/lang-sql';
|
|
import { oneDark } from '@codemirror/theme-one-dark';
|
|
import { Eye, Pencil, Save } from 'lucide-react';
|
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
|
|
export type CodeMode = 'text' | 'json' | 'xml' | 'code';
|
|
|
|
interface CodeEditorProps {
|
|
text: string;
|
|
mode: CodeMode;
|
|
/** Prism language id for syntax highlighting in view mode (used when mode='code'). */
|
|
syntaxHighlightLang?: string;
|
|
readOnly?: boolean;
|
|
onSave?: (text: string) => Promise<void>;
|
|
}
|
|
|
|
const cmTheme = EditorView.theme({
|
|
'&': { height: '100%', background: '#0a0a0a' },
|
|
'.cm-scroller': { overflow: 'auto', fontFamily: 'ui-monospace,monospace', fontSize: '12px', lineHeight: '1.5' },
|
|
'.cm-content': { padding: '12px 0' },
|
|
'.cm-focused': { outline: 'none' },
|
|
});
|
|
|
|
const langExt: Record<Exclude<CodeMode, 'code'>, any> = {
|
|
json: json(),
|
|
xml: xml(),
|
|
text: [],
|
|
};
|
|
|
|
function cmLangExt(prismLang: string | undefined): any {
|
|
switch (prismLang) {
|
|
case 'javascript': return javascript();
|
|
case 'typescript': return javascript({ typescript: true });
|
|
case 'jsx': return javascript({ jsx: true });
|
|
case 'tsx': return javascript({ jsx: true, typescript: true });
|
|
case 'python': return python();
|
|
case 'c':
|
|
case 'cpp': return cpp();
|
|
case 'css':
|
|
case 'scss': return css();
|
|
case 'rust': return rust();
|
|
case 'php': return php();
|
|
case 'sql': return sql();
|
|
default: return [];
|
|
}
|
|
}
|
|
|
|
const syntaxLang: Record<CodeMode, string> = {
|
|
text: 'text', json: 'json', xml: 'xml', code: 'text',
|
|
};
|
|
|
|
function prettify(text: string, mode: CodeMode): string {
|
|
if (mode === 'json') {
|
|
try { return JSON.stringify(JSON.parse(text), null, 2); } catch { /* fall through */ }
|
|
}
|
|
return text;
|
|
}
|
|
|
|
export default function CodeEditor({ text, mode, syntaxHighlightLang, readOnly = false, onSave }: CodeEditorProps) {
|
|
const [editMode, setEditMode] = useState(false);
|
|
const [editInitText, setEditInitText] = useState('');
|
|
const [saving, setSaving] = useState(false);
|
|
const editorViewRef = useRef<EditorView | null>(null);
|
|
|
|
const displayText = prettify(text, mode);
|
|
const extensions = [mode === 'code' ? cmLangExt(syntaxHighlightLang) : langExt[mode], cmTheme].flat();
|
|
|
|
const handleSave = async () => {
|
|
if (!editorViewRef.current || !onSave) return;
|
|
setSaving(true);
|
|
try { await onSave(editorViewRef.current.state.doc.toString()); }
|
|
finally { setSaving(false); }
|
|
};
|
|
|
|
if (!editMode) {
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
{!readOnly && (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-neutral-900 border-b border-neutral-700 flex-shrink-0 text-xs">
|
|
<button
|
|
onClick={() => { setEditInitText(displayText); setEditMode(true); }}
|
|
className="px-2 py-1 rounded bg-neutral-700 text-neutral-300 hover:bg-neutral-600 inline-flex items-center gap-1"
|
|
>
|
|
<Pencil className="w-3.5 h-3.5" /> Edit
|
|
</button>
|
|
</div>
|
|
)}
|
|
<div className="flex-1 overflow-auto text-xs">
|
|
<SyntaxHighlighter
|
|
language={syntaxHighlightLang ?? syntaxLang[mode]}
|
|
style={vscDarkPlus}
|
|
customStyle={{ margin: 0, minHeight: '100%', background: '#0a0a0a', fontSize: '12px', lineHeight: '1.5' }}
|
|
showLineNumbers
|
|
wrapLongLines
|
|
>
|
|
{displayText}
|
|
</SyntaxHighlighter>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-neutral-900 border-b border-neutral-700 flex-shrink-0 text-xs">
|
|
<button
|
|
onClick={() => setEditMode(false)}
|
|
className="px-2 py-1 rounded bg-amber-600 text-white inline-flex items-center gap-1"
|
|
>
|
|
<Eye className="w-3.5 h-3.5" /> View
|
|
</button>
|
|
{onSave && (
|
|
<button
|
|
onClick={() => void handleSave()}
|
|
disabled={saving}
|
|
className="px-2 py-1 rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 inline-flex items-center gap-1"
|
|
>
|
|
<Save className="w-3.5 h-3.5" /> {saving ? 'Saving…' : 'Save'}
|
|
</button>
|
|
)}
|
|
<span className="text-neutral-600 ml-auto">Ctrl+Z/Y undo · Ctrl+F search</span>
|
|
</div>
|
|
<div className="flex-1 overflow-hidden">
|
|
<CodeMirror
|
|
value={editInitText}
|
|
extensions={extensions}
|
|
theme={oneDark}
|
|
height="100%"
|
|
onCreateEditor={view => { editorViewRef.current = view; }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|