import { useRef, useCallback, useEffect } from 'react';
import Editor, { OnMount, BeforeMount } from '@monaco-editor/react';
import type { editor } from 'monaco-editor';
import { findPathLineNumbers } from '../../utils';
interface JsonEditorProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
readOnly?: boolean;
className?: string;
theme?: 'light' | 'dark';
onCursorChange?: (line: number, column: number) => void;
/** Paths to highlight in the editor (e.g., ["data.user.name", "context.metadata"]) */
highlightedPaths?: string[];
}
// Define VSCode-like themes
const defineThemes: BeforeMount = (monaco) => {
// VSCode Dark+ theme
monaco.editor.defineTheme('vscode-dark', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'string.key.json', foreground: '9CDCFE' },
{ token: 'string.value.json', foreground: 'CE9178' },
{ token: 'number', foreground: 'B5CEA8' },
{ token: 'keyword', foreground: '569CD6' },
{ token: 'delimiter', foreground: 'D4D4D4' },
],
colors: {
'editor.background': '#1e1e1e',
'editor.foreground': '#d4d4d4',
'editor.lineHighlightBackground': '#2d2d2d',
'editor.selectionBackground': '#264f78',
'editorCursor.foreground': '#aeafad',
'editorLineNumber.foreground': '#858585',
'editorLineNumber.activeForeground': '#c6c6c6',
'editorIndentGuide.background': '#404040',
'editorIndentGuide.activeBackground': '#707070',
'editor.selectionHighlightBackground': '#3a3d41',
'editorBracketMatch.background': '#0064001a',
'editorBracketMatch.border': '#888888',
'editorGutter.background': '#1e1e1e',
'scrollbarSlider.background': '#79797966',
'scrollbarSlider.hoverBackground': '#646464b3',
'scrollbarSlider.activeBackground': '#bfbfbf66',
'minimap.background': '#1e1e1e',
},
});
// VSCode Light+ theme
monaco.editor.defineTheme('vscode-light', {
base: 'vs',
inherit: true,
rules: [
{ token: 'string.key.json', foreground: '0451A5' },
{ token: 'string.value.json', foreground: 'A31515' },
{ token: 'number', foreground: '098658' },
{ token: 'keyword', foreground: '0000FF' },
{ token: 'delimiter', foreground: '000000' },
],
colors: {
'editor.background': '#ffffff',
'editor.foreground': '#000000',
'editor.lineHighlightBackground': '#f5f5f5',
'editor.selectionBackground': '#add6ff',
'editorCursor.foreground': '#000000',
'editorLineNumber.foreground': '#999999',
'editorLineNumber.activeForeground': '#000000',
'editorIndentGuide.background': '#d3d3d3',
'editorIndentGuide.activeBackground': '#939393',
'editor.selectionHighlightBackground': '#add6ff4d',
'editorBracketMatch.background': '#0064001a',
'editorBracketMatch.border': '#b9b9b9',
'editorGutter.background': '#ffffff',
'scrollbarSlider.background': '#64646466',
'scrollbarSlider.hoverBackground': '#646464b3',
'scrollbarSlider.activeBackground': '#00000099',
'minimap.background': '#ffffff',
},
});
};
export function JsonEditor({
value,
onChange,
readOnly = false,
className = '',
theme = 'dark',
onCursorChange,
highlightedPaths,
}: JsonEditorProps) {
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const decorationsRef = useRef<string[]>([]);
const handleEditorMount: OnMount = useCallback((editor, monaco) => {
editorRef.current = editor;
// Configure JSON validation
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: [],
allowComments: false,
trailingCommas: 'error',
});
// Add cursor position listener
if (onCursorChange) {
editor.onDidChangeCursorPosition((e) => {
onCursorChange(e.position.lineNumber, e.position.column);
});
}
// Focus the editor only if not readOnly
if (!readOnly) {
editor.focus();
}
}, [onCursorChange, readOnly]);
// Apply line decorations for highlighted paths
useEffect(() => {
if (!editorRef.current || !highlightedPaths || highlightedPaths.length === 0) {
// Clear decorations if no paths
if (editorRef.current && decorationsRef.current.length > 0) {
decorationsRef.current = editorRef.current.deltaDecorations(decorationsRef.current, []);
}
return;
}
const lineNumbers = findPathLineNumbers(value, highlightedPaths);
if (lineNumbers.length > 0) {
const decorations: editor.IModelDeltaDecoration[] = lineNumbers.map(lineNumber => ({
range: {
startLineNumber: lineNumber,
startColumn: 1,
endLineNumber: lineNumber,
endColumn: 1,
},
options: {
isWholeLine: true,
className: 'df-highlighted-line',
glyphMarginClassName: 'df-highlighted-glyph',
overviewRuler: {
color: theme === 'dark' ? '#4ec9b0' : '#388a34',
position: 1, // Left
},
},
}));
decorationsRef.current = editorRef.current.deltaDecorations(
decorationsRef.current,
decorations
);
} else {
// Clear decorations
decorationsRef.current = editorRef.current.deltaDecorations(decorationsRef.current, []);
}
}, [value, highlightedPaths, theme]);
const handleChange = useCallback((newValue: string | undefined) => {
onChange(newValue || '');
}, [onChange]);
return (
<div className={`df-monaco-editor-wrapper ${className}`}>
<Editor
height="100%"
defaultLanguage="json"
value={value}
onChange={handleChange}
onMount={handleEditorMount}
beforeMount={defineThemes}
theme={theme === 'dark' ? 'vscode-dark' : 'vscode-light'}
options={{
readOnly,
minimap: { enabled: false },
fontSize: 13,
fontFamily: "'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace",
lineHeight: 20,
tabSize: 2,
insertSpaces: true,
automaticLayout: true,
scrollBeyondLastLine: false,
wordWrap: 'on',
wrappingIndent: 'indent',
folding: true,
foldingStrategy: 'indentation',
showFoldingControls: 'mouseover',
bracketPairColorization: { enabled: true },
guides: {
bracketPairs: false,
indentation: false,
highlightActiveBracketPair: true,
highlightActiveIndentation: false,
},
renderLineHighlight: readOnly ? 'none' : 'line',
selectOnLineNumbers: true,
roundedSelection: true,
cursorBlinking: 'smooth',
cursorSmoothCaretAnimation: 'on',
smoothScrolling: true,
padding: { top: 8, bottom: 8 },
scrollbar: {
vertical: 'auto',
horizontal: 'auto',
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
},
overviewRulerBorder: false,
hideCursorInOverviewRuler: true,
contextmenu: true,
quickSuggestions: false,
suggestOnTriggerCharacters: false,
acceptSuggestionOnEnter: 'off',
formatOnPaste: true,
formatOnType: false,
glyphMargin: false,
}}
/>
</div>
);
}