Skip to content

Commit b46d92e

Browse files
authored
upgrade monaco editor (remove monaco-editor/loader and monaco-editor/react) (#2743)
* Updates monaco-editor to new version. * removes weird AMD stuff, to move to ESM via vite * remove monaco-editor/loader * remove monaco-editor/react * implement the more native monaco wrappers ourselves
1 parent 5b94b95 commit b46d92e

File tree

15 files changed

+375
-288
lines changed

15 files changed

+375
-288
lines changed

electron.vite.config.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import tailwindcss from "@tailwindcss/vite";
55
import react from "@vitejs/plugin-react-swc";
66
import { defineConfig } from "electron-vite";
77
import { ViteImageOptimizer } from "vite-plugin-image-optimizer";
8-
import { viteStaticCopy } from "vite-plugin-static-copy";
98
import svgr from "vite-plugin-svgr";
109
import tsconfigPaths from "vite-tsconfig-paths";
1110

@@ -134,8 +133,7 @@ export default defineConfig({
134133
manualChunks(id) {
135134
const p = id.replace(/\\/g, "/");
136135
if (p.includes("node_modules/monaco") || p.includes("node_modules/@monaco")) return "monaco";
137-
if (p.includes("node_modules/mermaid") || p.includes("node_modules/@mermaid"))
138-
return "mermaid";
136+
if (p.includes("node_modules/mermaid") || p.includes("node_modules/@mermaid")) return "mermaid";
139137
if (p.includes("node_modules/katex") || p.includes("node_modules/@katex")) return "katex";
140138
if (p.includes("node_modules/shiki") || p.includes("node_modules/@shiki")) {
141139
return "shiki";
@@ -153,7 +151,17 @@ export default defineConfig({
153151
server: {
154152
open: false,
155153
watch: {
156-
ignored: ["dist/**", "**/*.go", "**/go.mod", "**/go.sum", "**/*.md", "**/*.json", "emain/**"],
154+
ignored: [
155+
"dist/**",
156+
"**/*.go",
157+
"**/go.mod",
158+
"**/go.sum",
159+
"**/*.md",
160+
"**/*.json",
161+
"emain/**",
162+
"**/*.txt",
163+
"**/*.log",
164+
],
157165
},
158166
},
159167
css: {
@@ -172,9 +180,6 @@ export default defineConfig({
172180
}),
173181
react({}),
174182
tailwindcss(),
175-
viteStaticCopy({
176-
targets: [{ src: "node_modules/monaco-editor/min/vs/*", dest: "monaco" }],
177-
}),
178183
],
179184
},
180185
});

frontend/app/monaco/monaco-env.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import * as monaco from "monaco-editor";
5+
import "monaco-editor/esm/vs/language/css/monaco.contribution";
6+
import "monaco-editor/esm/vs/language/html/monaco.contribution";
7+
import "monaco-editor/esm/vs/language/json/monaco.contribution";
8+
import "monaco-editor/esm/vs/language/typescript/monaco.contribution";
9+
import { configureMonacoYaml } from "monaco-yaml";
10+
11+
import { MonacoSchemas } from "@/app/monaco/schemaendpoints";
12+
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
13+
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
14+
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
15+
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
16+
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
17+
import ymlWorker from "./yamlworker?worker";
18+
19+
let monacoConfigured = false;
20+
21+
window.MonacoEnvironment = {
22+
getWorker(_, label) {
23+
if (label === "json") {
24+
return new jsonWorker();
25+
}
26+
if (label === "css" || label === "scss" || label === "less") {
27+
return new cssWorker();
28+
}
29+
if (label === "yaml" || label === "yml") {
30+
return new ymlWorker();
31+
}
32+
if (label === "html" || label === "handlebars" || label === "razor") {
33+
return new htmlWorker();
34+
}
35+
if (label === "typescript" || label === "javascript") {
36+
return new tsWorker();
37+
}
38+
return new editorWorker();
39+
},
40+
};
41+
42+
export function loadMonaco() {
43+
if (monacoConfigured) {
44+
return;
45+
}
46+
monacoConfigured = true;
47+
monaco.editor.defineTheme("wave-theme-dark", {
48+
base: "vs-dark",
49+
inherit: true,
50+
rules: [],
51+
colors: {
52+
"editor.background": "#00000000",
53+
"editorStickyScroll.background": "#00000055",
54+
"minimap.background": "#00000077",
55+
focusBorder: "#00000000",
56+
},
57+
});
58+
monaco.editor.defineTheme("wave-theme-light", {
59+
base: "vs",
60+
inherit: true,
61+
rules: [],
62+
colors: {
63+
"editor.background": "#fefefe",
64+
focusBorder: "#00000000",
65+
},
66+
});
67+
configureMonacoYaml(monaco, {
68+
validate: true,
69+
schemas: [],
70+
});
71+
monaco.editor.setTheme("wave-theme-dark");
72+
// Disable default validation errors for typescript and javascript
73+
monaco.typescript.typescriptDefaults.setDiagnosticsOptions({
74+
noSemanticValidation: true,
75+
});
76+
monaco.json.jsonDefaults.setDiagnosticsOptions({
77+
validate: true,
78+
allowComments: false,
79+
enableSchemaRequest: true,
80+
schemas: MonacoSchemas,
81+
});
82+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { loadMonaco } from "@/app/monaco/monaco-env";
5+
import type * as MonacoTypes from "monaco-editor";
6+
import * as monaco from "monaco-editor";
7+
import { useEffect, useRef } from "react";
8+
9+
function createModel(value: string, path: string, language?: string) {
10+
const uri = monaco.Uri.parse(`wave://editor/${encodeURIComponent(path)}`);
11+
return monaco.editor.createModel(value, language, uri);
12+
}
13+
14+
type CodeEditorProps = {
15+
text: string;
16+
readonly: boolean;
17+
language?: string;
18+
onChange?: (text: string) => void;
19+
onMount?: (editor: MonacoTypes.editor.IStandaloneCodeEditor, monacoApi: typeof monaco) => () => void;
20+
path: string;
21+
options: MonacoTypes.editor.IEditorOptions;
22+
};
23+
24+
export function MonacoCodeEditor({
25+
text,
26+
readonly,
27+
language,
28+
onChange,
29+
onMount,
30+
path,
31+
options,
32+
}: CodeEditorProps) {
33+
const divRef = useRef<HTMLDivElement>(null);
34+
const editorRef = useRef<MonacoTypes.editor.IStandaloneCodeEditor | null>(null);
35+
const onUnmountRef = useRef<(() => void) | null>(null);
36+
const applyingFromProps = useRef(false);
37+
38+
useEffect(() => {
39+
loadMonaco();
40+
41+
const el = divRef.current;
42+
if (!el) return;
43+
44+
const model = createModel(text, path, language);
45+
console.log("[monaco] CREATE MODEL", path, model);
46+
47+
const editor = monaco.editor.create(el, {
48+
...options,
49+
readOnly: readonly,
50+
model,
51+
});
52+
editorRef.current = editor;
53+
54+
const sub = model.onDidChangeContent(() => {
55+
if (applyingFromProps.current) return;
56+
onChange?.(model.getValue());
57+
});
58+
59+
if (onMount) {
60+
onUnmountRef.current = onMount(editor, monaco);
61+
}
62+
63+
return () => {
64+
sub.dispose();
65+
if (onUnmountRef.current) onUnmountRef.current();
66+
editor.dispose();
67+
model.dispose();
68+
console.log("[monaco] dispose model");
69+
editorRef.current = null;
70+
};
71+
// mount/unmount only
72+
// eslint-disable-next-line react-hooks/exhaustive-deps
73+
}, []);
74+
75+
// Keep model value in sync with props
76+
useEffect(() => {
77+
const editor = editorRef.current;
78+
if (!editor) return;
79+
const model = editor.getModel();
80+
if (!model) return;
81+
82+
const current = model.getValue();
83+
if (current === text) return;
84+
85+
applyingFromProps.current = true;
86+
model.pushEditOperations([], [{ range: model.getFullModelRange(), text }], () => null);
87+
applyingFromProps.current = false;
88+
}, [text]);
89+
90+
// Keep options in sync
91+
useEffect(() => {
92+
const editor = editorRef.current;
93+
if (!editor) return;
94+
editor.updateOptions({ ...options, readOnly: readonly });
95+
}, [options, readonly]);
96+
97+
// Keep language in sync
98+
useEffect(() => {
99+
const editor = editorRef.current;
100+
if (!editor) return;
101+
const model = editor.getModel();
102+
if (!model || !language) return;
103+
monaco.editor.setModelLanguage(model, language);
104+
}, [language]);
105+
106+
return <div className="flex flex-col h-full w-full" ref={divRef} />;
107+
}
108+
109+
type DiffViewerProps = {
110+
original: string;
111+
modified: string;
112+
language?: string;
113+
path: string;
114+
options: MonacoTypes.editor.IDiffEditorOptions;
115+
};
116+
117+
export function MonacoDiffViewer({ original, modified, language, path, options }: DiffViewerProps) {
118+
const divRef = useRef<HTMLDivElement>(null);
119+
const diffRef = useRef<MonacoTypes.editor.IStandaloneDiffEditor | null>(null);
120+
121+
// Create once
122+
useEffect(() => {
123+
loadMonaco();
124+
125+
const el = divRef.current;
126+
if (!el) return;
127+
128+
const origUri = monaco.Uri.parse(`wave://diff/${encodeURIComponent(path)}.orig`);
129+
const modUri = monaco.Uri.parse(`wave://diff/${encodeURIComponent(path)}.mod`);
130+
131+
const originalModel = monaco.editor.createModel(original, language, origUri);
132+
const modifiedModel = monaco.editor.createModel(modified, language, modUri);
133+
134+
const diff = monaco.editor.createDiffEditor(el, options);
135+
diffRef.current = diff;
136+
137+
diff.setModel({ original: originalModel, modified: modifiedModel });
138+
139+
return () => {
140+
diff.dispose();
141+
originalModel.dispose();
142+
modifiedModel.dispose();
143+
diffRef.current = null;
144+
};
145+
// eslint-disable-next-line react-hooks/exhaustive-deps
146+
}, []);
147+
148+
// Update models on prop change
149+
useEffect(() => {
150+
const diff = diffRef.current;
151+
if (!diff) return;
152+
const model = diff.getModel();
153+
if (!model) return;
154+
155+
if (model.original.getValue() !== original) model.original.setValue(original);
156+
if (model.modified.getValue() !== modified) model.modified.setValue(modified);
157+
158+
if (language) {
159+
monaco.editor.setModelLanguage(model.original, language);
160+
monaco.editor.setModelLanguage(model.modified, language);
161+
}
162+
}, [original, modified, language]);
163+
164+
useEffect(() => {
165+
const diff = diffRef.current;
166+
if (!diff) return;
167+
diff.updateOptions(options);
168+
}, [options]);
169+
170+
return <div className="flex flex-col h-full w-full" ref={divRef} />;
171+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import settingsSchema from "../../../schema/settings.json";
5+
import connectionsSchema from "../../../schema/connections.json";
6+
import aipresetsSchema from "../../../schema/aipresets.json";
7+
import bgpresetsSchema from "../../../schema/bgpresets.json";
8+
import waveaiSchema from "../../../schema/waveai.json";
9+
import widgetsSchema from "../../../schema/widgets.json";
10+
11+
type SchemaInfo = {
12+
uri: string;
13+
fileMatch: Array<string>;
14+
schema: object;
15+
};
16+
17+
const MonacoSchemas: SchemaInfo[] = [
18+
{
19+
uri: "wave://schema/settings.json",
20+
fileMatch: ["*/WAVECONFIGPATH/settings.json"],
21+
schema: settingsSchema,
22+
},
23+
{
24+
uri: "wave://schema/connections.json",
25+
fileMatch: ["*/WAVECONFIGPATH/connections.json"],
26+
schema: connectionsSchema,
27+
},
28+
{
29+
uri: "wave://schema/aipresets.json",
30+
fileMatch: ["*/WAVECONFIGPATH/presets/ai.json"],
31+
schema: aipresetsSchema,
32+
},
33+
{
34+
uri: "wave://schema/bgpresets.json",
35+
fileMatch: ["*/WAVECONFIGPATH/presets/bg.json"],
36+
schema: bgpresetsSchema,
37+
},
38+
{
39+
uri: "wave://schema/waveai.json",
40+
fileMatch: ["*/WAVECONFIGPATH/waveai.json"],
41+
schema: waveaiSchema,
42+
},
43+
{
44+
uri: "wave://schema/widgets.json",
45+
fileMatch: ["*/WAVECONFIGPATH/widgets.json"],
46+
schema: widgetsSchema,
47+
},
48+
];
49+
50+
export { MonacoSchemas };
File renamed without changes.

0 commit comments

Comments
 (0)