diff --git a/src/components/CodeEditor/index.jsx b/src/components/CodeEditor/index.jsx
index df34824ee..26409170e 100644
--- a/src/components/CodeEditor/index.jsx
+++ b/src/components/CodeEditor/index.jsx
@@ -1,4 +1,5 @@
import { Editor } from "@monaco-editor/react";
+import { useEffect, useRef } from "react";
import { useDiagram, useSettings } from "../../hooks";
import { Button, Toast } from "@douyinfe/semi-ui";
import { useTranslation } from "react-i18next";
@@ -9,6 +10,7 @@ import "./styles.css";
export default function CodeEditor({
showCopyButton,
extraControls,
+ markers = [],
...props
}) {
const { settings } = useSettings();
@@ -24,14 +26,49 @@ export default function CodeEditor({
});
};
+ const editorRef = useRef(null);
+ const monacoRef = useRef(null);
+
const handleEditorMount = (editor, monaco) => {
setUpDBML(monaco, database);
setTimeout(() => {
editor.getAction("editor.action.formatDocument").run();
}, 300);
+
+ editorRef.current = editor;
+ monacoRef.current = monaco;
+
+ // Apply existing markers after mount
+ try {
+ const model = editor.getModel();
+ if (model) {
+ const normalized = markers.map((m) => ({
+ severity: m.severity ?? monaco.MarkerSeverity.Error,
+ ...m,
+ }));
+ monaco.editor.setModelMarkers(model, "dbml", normalized);
+ }
+ } catch (err) {
+ console.warn("Failed to set initial markers", err);
+ }
};
+ useEffect(() => {
+ if (!editorRef.current || !monacoRef.current) return;
+ try {
+ const model = editorRef.current.getModel();
+ if (!model) return;
+ const normalized = markers.map((m) => ({
+ severity: m.severity ?? monacoRef.current.MarkerSeverity.Error,
+ ...m,
+ }));
+ monacoRef.current.editor.setModelMarkers(model, "dbml", normalized);
+ } catch (err) {
+ console.warn("Failed to update markers", err);
+ }
+ }, [markers]);
+
return (
toDBML({ ...diagram, enums }));
const { setLayout } = useLayout();
+ const { setExternalIssues } = diagram;
const { t } = useTranslation();
+ // Translate DBML parse errors to issues and Monaco markers
+ const [markers, setMarkers] = useState([]);
+
const toggleDBMLEditor = () => {
setLayout((prev) => ({ ...prev, dbmlEditor: !prev.dbmlEditor }));
};
useEffect(() => {
- setValue(toDBML({ tables: currentTables, enums, relationships }));
- }, [currentTables, enums, relationships]);
+ const normalized = toDBML({
+ tables: currentTables,
+ enums,
+ relationships,
+ database,
+ });
+ setValue(normalized);
+ }, [currentTables, enums, relationships, database]);
+
+ useEffect(() => {
+ const currentDbml = toDBML({
+ tables: currentTables,
+ enums,
+ relationships,
+ database,
+ });
+
+ if (value === currentDbml) {
+ // If editor content already matches diagram state,
+ // ensure any lingering external issues/markers are cleared
+ if (externalIssues?.length) setExternalIssues([]);
+ if (markers.length) setMarkers([]);
+ return;
+ }
+
+ const handle = setTimeout(() => {
+ try {
+ const parsed = fromDBML(value);
+ // Preserve coordinates when table names match existing ones
+ const nameToExisting = new Map(
+ currentTables.map((t) => [t.name, { x: t.x, y: t.y }]),
+ );
+ parsed.tables = parsed.tables.map((t) => {
+ const coords = nameToExisting.get(t.name);
+ return coords ? { ...t, ...coords } : t;
+ });
+ setTables(parsed.tables);
+ setRelationships(parsed.relationships);
+ setEnums(parsed.enums);
+ // Clear any previous external issues on success
+ setExternalIssues([]);
+ setMarkers([]);
+ } catch (err) {
+ const { issues: parsedIssues, markers: parsedMarkers } =
+ produceDiagnostics(err);
+ setExternalIssues(parsedIssues);
+ setMarkers(parsedMarkers);
+ }
+ }, 700);
+
+ return () => clearTimeout(handle);
+ }, [
+ value,
+ currentTables,
+ enums,
+ relationships,
+ database,
+ setTables,
+ setRelationships,
+ setEnums,
+ setExternalIssues,
+ externalIssues?.length,
+ markers.length,
+ ]);
+
+ const produceDiagnostics = (err) => {
+ // Prefer diagnostics from @dbml/core if present
+ if (Array.isArray(err?.diags) && err.diags.length > 0) {
+ const issues = err.diags.map((d) => {
+ const ln = d?.location?.start?.line;
+ const col = d?.location?.start?.column;
+ const code = d?.code ? ` [${d.code}]` : "";
+ if (ln && col) return `line ${ln}, col ${col}: ${d.message}${code}`;
+ return d.message + code;
+ });
+
+ const markers = err.diags.map((d) => {
+ const start = d?.location?.start || {};
+ const end = d?.location?.end || {};
+ const startLineNumber = start.line || 1;
+ const startColumn = start.column || 1;
+ const endLineNumber = end.line || startLineNumber;
+ const endColumn = end.column || startColumn + 1;
+ return {
+ startLineNumber,
+ startColumn,
+ endLineNumber,
+ endColumn,
+ message: d.message,
+ };
+ });
+ return { issues, markers };
+ }
+
+ // Fallbacks
+ const message =
+ (typeof err?.message === "string" && err.message) ||
+ (typeof err?.description === "string" && err.description) ||
+ (() => {
+ try {
+ return JSON.stringify(err);
+ } catch {
+ return String(err);
+ }
+ })();
+
+ // Try to extract line/column from string messages
+ const m =
+ /line\s+(\d+)\s*,\s*column\s*(\d+)/i.exec(message) ||
+ /\((\d+)\s*[:|,]\s*(\d+)\)/.exec(message);
+ const ln = m ? parseInt(m[1], 10) : 1;
+ const col = m ? parseInt(m[2], 10) : 1;
+ return {
+ issues: [message],
+ markers: [
+ {
+ startLineNumber: ln,
+ startColumn: col,
+ endLineNumber: ln,
+ endColumn: col + 1,
+ message,
+ },
+ ],
+ };
+ };
return (
{
- const findIssues = async () => {
- const newIssues = getIssues({
- tables: tables,
- relationships: relationships,
- types: types,
- database: database,
- enums: enums,
- });
+ const newIssues = getIssues({
+ tables: tables,
+ relationships: relationships,
+ types: types,
+ database: database,
+ enums: enums,
+ });
- if (!arrayIsEqual(newIssues, issues)) {
- setIssues(newIssues);
- }
- };
+ const combined = [...externalIssues, ...newIssues];
- findIssues();
- }, [tables, relationships, issues, types, database, enums]);
+ if (!arrayIsEqual(combined, issues)) {
+ setIssues(combined);
+ }
+ }, [tables, relationships, issues, types, database, enums, externalIssues]);
return (
@@ -54,11 +52,25 @@ export default function Issues() {
{t("strict_mode_is_on_no_issues")}
) : issues.length > 0 ? (
<>
- {issues.map((e, i) => (
-
- {e}
-
- ))}
+ {issues.map((e, i) => {
+ const text =
+ typeof e === "string"
+ ? e
+ : typeof e?.message === "string"
+ ? e.message
+ : (() => {
+ try {
+ return JSON.stringify(e);
+ } catch {
+ return String(e);
+ }
+ })();
+ return (
+
+ {text}
+
+ );
+ })}
>
) : (
{t("no_issues")}
diff --git a/src/context/DiagramContext.jsx b/src/context/DiagramContext.jsx
index 5e6b54e0a..e7abcdbad 100644
--- a/src/context/DiagramContext.jsx
+++ b/src/context/DiagramContext.jsx
@@ -12,6 +12,7 @@ export default function DiagramContextProvider({ children }) {
const [database, setDatabase] = useState(DB.GENERIC);
const [tables, setTables] = useState([]);
const [relationships, setRelationships] = useState([]);
+ const [externalIssues, setExternalIssues] = useState([]);
const { transform } = useTransform();
const { setUndoStack, setRedoStack } = useUndoRedo();
const { selectedElement, setSelectedElement } = useSelect();
@@ -244,6 +245,8 @@ export default function DiagramContextProvider({ children }) {
setDatabase,
tablesCount: tables.length,
relationshipsCount: relationships.length,
+ externalIssues,
+ setExternalIssues,
}}
>
{children}