diff --git a/gui/package.json b/gui/package.json index b0c19147..72015cca 100644 --- a/gui/package.json +++ b/gui/package.json @@ -29,7 +29,6 @@ "@octokit/rest": "^21.0.0", "@vercel/analytics": "^1.3.1", "jszip": "^3.10.1", - "monaco-editor": "^0.52.2", "plotly.js-cartesian-dist": "^2.35.2", "pyodide": "^0.27.2", "react": "^19.0.0", @@ -58,6 +57,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "jsdom": "^24.1.0", + "monaco-editor": "^0.52.2", "prettier": "^3.3.2", "typescript": "^5.0.2", "vite": "^6.0.11", diff --git a/gui/src/app/components/FileEditor/ScriptEditor.tsx b/gui/src/app/components/FileEditor/ScriptEditor.tsx index 63769bc4..5cd8497e 100644 --- a/gui/src/app/components/FileEditor/ScriptEditor.tsx +++ b/gui/src/app/components/FileEditor/ScriptEditor.tsx @@ -1,15 +1,18 @@ +import { FunctionComponent, RefObject, useCallback, use, useMemo } from "react"; + import { Help, PlayArrow } from "@mui/icons-material"; import Box from "@mui/material/Box"; -import TextEditor from "@SpComponents/FileEditor/TextEditor"; +import { Split } from "@geoffcox/react-splitter"; +import { useMonaco } from "@monaco-editor/react"; +import { type editor } from "monaco-editor"; + import { ColorOptions, ToolbarItem } from "@SpComponents/FileEditor/ToolBar"; import { FileNames } from "@SpCore/Project/FileMapping"; import { ProjectContext } from "@SpCore/Project/ProjectContextProvider"; import { ProjectKnownFiles } from "@SpCore/Project/ProjectDataModel"; import { normalizeLineEndings } from "@SpUtil/normalizeLineEndings"; -import { FunctionComponent, RefObject, useCallback, use, useMemo } from "react"; import { InterpreterStatus } from "@SpCore/Scripting/InterpreterTypes"; -import { Split } from "@geoffcox/react-splitter"; -import { editor, KeyCode, KeyMod } from "monaco-editor"; +import TextEditor from "@SpComponents/FileEditor/TextEditor"; const interpreterNames = { python: "pyodide", r: "webR" } as const; @@ -69,21 +72,27 @@ const ScriptEditor: FunctionComponent = ({ return content !== editedContent; }, [content, editedContent]); - const runCtrlEnter: editor.IActionDescriptor[] = useMemo( - () => [ + const monacoInstance = useMonaco(); + + const runCtrlEnter: editor.IActionDescriptor[] = useMemo(() => { + if (!monacoInstance) { + return []; + } + return [ { id: "run-script", label: "Run Script", - keybindings: [KeyMod.CtrlCmd | KeyCode.Enter], + keybindings: [ + monacoInstance.KeyMod.CtrlCmd | monacoInstance.KeyCode.Enter, + ], run: () => { if (runnable && !unsavedChanges) { runCode(); } }, }, - ], - [runCode, runnable, unsavedChanges], - ); + ]; + }, [monacoInstance, runCode, runnable, unsavedChanges]); const toolbarItems: ToolbarItem[] = useMemo(() => { return makeToolbar({ diff --git a/gui/src/app/components/FileEditor/TextEditor.tsx b/gui/src/app/components/FileEditor/TextEditor.tsx index 9aeccbfc..91f078f2 100644 --- a/gui/src/app/components/FileEditor/TextEditor.tsx +++ b/gui/src/app/components/FileEditor/TextEditor.tsx @@ -1,14 +1,3 @@ -import { Editor, loader, useMonaco } from "@monaco-editor/react"; - -import { UserSettingsContext } from "@SpCore/Settings/UserSettings"; -import { CodeMarker } from "@SpCore/Stanc/Linting"; -import { - editor, - IDisposable, - KeyCode, - KeyMod, - MarkerSeverity, -} from "monaco-editor"; import { FunctionComponent, use, @@ -18,9 +7,27 @@ import { useState, } from "react"; -import monacoAddStanLang from "./monacoStanLanguage"; +import Loading from "@SpComponents/Loading"; +import { UserSettingsContext } from "@SpCore/Settings/UserSettings"; +import { CodeMarker } from "@SpCore/Stanc/Linting"; +import { unreachable } from "@SpUtil/unreachable"; + +import { Editor, loader, useMonaco, type Monaco } from "@monaco-editor/react"; +import type { editor, IDisposable } from "monaco-editor"; + import { ToolBar, ToolbarItem } from "./ToolBar"; +import monacoAddStanLang from "./monacoStanLanguage"; +// loader from @monaco-editor/react handles the loading of the monaco editor +// importantly, it downloads from a CDN, so we need to make sure our +// dependency on the monaco-editor package is limited to types only, +// to avoid downloading twice. + +loader.config({ + paths: { + vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs", + }, +}); loader.init().then(monacoAddStanLang); type Props = { @@ -86,7 +93,7 @@ const TextEditor: FunctionComponent = ({ const modelMarkers = codeMarkers.map((marker) => ({ ...marker, - severity: toMonacoMarkerSeverity[marker.severity], + severity: toMonacoMarkerSeverity(marker.severity, monacoInstance), })); monacoInstance.editor.setModelMarkers( @@ -97,24 +104,34 @@ const TextEditor: FunctionComponent = ({ }, [codeMarkers, monacoInstance, editorInstance]); useEffect(() => { - if (!editorInstance) return; + if (!editorInstance || !monacoInstance) return; if (!contentOnEmpty) return; if (text || editedText) { return; } - const contentWidget = createHintTextContentWidget(contentOnEmpty); + const widgetPosition = { + position: { lineNumber: 1, column: 1 }, + preference: [monacoInstance.editor.ContentWidgetPositionPreference.EXACT], + }; + const contentWidget = createHintTextContentWidget( + contentOnEmpty, + widgetPosition, + ); editorInstance.addContentWidget(contentWidget); return () => { editorInstance.removeContentWidget(contentWidget); }; - }, [text, editorInstance, editedText, contentOnEmpty]); + }, [text, editorInstance, editedText, contentOnEmpty, monacoInstance]); useEffect(() => { - if (!editorInstance) return; + if (!editorInstance || !monacoInstance) return; + const disposable = editorInstance.addAction({ id: "save", label: "Save", - keybindings: [KeyMod.CtrlCmd | KeyCode.KeyS], + keybindings: [ + monacoInstance.KeyMod.CtrlCmd | monacoInstance.KeyCode.KeyS, + ], run: () => { if (!readOnly) { onSaveText(); @@ -124,7 +141,7 @@ const TextEditor: FunctionComponent = ({ return () => { disposable.dispose(); }; - }, [editorInstance, onSaveText, readOnly]); + }, [editorInstance, monacoInstance, onSaveText, readOnly]); useEffect(() => { if (!editorInstance) return; @@ -160,8 +177,10 @@ const TextEditor: FunctionComponent = ({ /> setEditor(editor)} + loading={} options={{ readOnly, domReadOnly: readOnly, @@ -175,14 +194,28 @@ const TextEditor: FunctionComponent = ({ ); }; -const toMonacoMarkerSeverity = { - error: MarkerSeverity.Error, - warning: MarkerSeverity.Warning, - hint: MarkerSeverity.Hint, - info: MarkerSeverity.Info, -} as const; +const toMonacoMarkerSeverity = ( + severity: CodeMarker["severity"], + monacoInstance: Monaco, +) => { + switch (severity) { + case "error": + return monacoInstance.MarkerSeverity.Error; + case "warning": + return monacoInstance.MarkerSeverity.Warning; + case "hint": + return monacoInstance.MarkerSeverity.Hint; + case "info": + return monacoInstance.MarkerSeverity.Info; + default: + return unreachable(severity); + } +}; -const createHintTextContentWidget = (content: string | HTMLSpanElement) => { +const createHintTextContentWidget = ( + content: string | HTMLSpanElement, + position: editor.IContentWidgetPosition, +) => { return { getDomNode: () => { const node = document.createElement("div"); @@ -197,12 +230,7 @@ const createHintTextContentWidget = (content: string | HTMLSpanElement) => { return node; }, getId: () => "hintText", - getPosition: () => { - return { - position: { lineNumber: 1, column: 1 }, - preference: [editor.ContentWidgetPositionPreference.EXACT], - }; - }, + getPosition: () => position, }; }; diff --git a/gui/src/app/components/FileEditor/monacoStanLanguage.ts b/gui/src/app/components/FileEditor/monacoStanLanguage.ts index a0c9a917..8d27f331 100644 --- a/gui/src/app/components/FileEditor/monacoStanLanguage.ts +++ b/gui/src/app/components/FileEditor/monacoStanLanguage.ts @@ -3,8 +3,8 @@ // See https://microsoft.github.io/monaco-editor/monarch.html // Adapted in part from https://github.com/WardBrian/vscode-stan-extension/blob/main/lang/syntaxes/stan.json -import { Monaco } from "@monaco-editor/react"; -import { languages } from "monaco-editor"; +import type { Monaco } from "@monaco-editor/react"; +import type { languages } from "monaco-editor"; const BLOCKS = [ "functions", diff --git a/gui/src/app/components/LazyPlotlyPlot.tsx b/gui/src/app/components/LazyPlotlyPlot.tsx index 15626e2d..a2edf29f 100644 --- a/gui/src/app/components/LazyPlotlyPlot.tsx +++ b/gui/src/app/components/LazyPlotlyPlot.tsx @@ -1,7 +1,8 @@ -import CircularProgress from "@mui/material/CircularProgress"; import React, { FunctionComponent, Suspense, useMemo } from "react"; import useMeasure from "react-use-measure"; +import Loading from "@SpComponents/Loading"; + import type { PlotParams } from "react-plotly.js"; import createPlotlyComponent from "react-plotly.js/factory"; const Plot = React.lazy(async () => { @@ -23,14 +24,7 @@ const LazyPlotlyPlot: FunctionComponent = ({ data, layout }) => { return (
- - -

Loading Plotly.js

-
- } - > + }> diff --git a/gui/src/app/components/Loading.tsx b/gui/src/app/components/Loading.tsx new file mode 100644 index 00000000..d7cd1954 --- /dev/null +++ b/gui/src/app/components/Loading.tsx @@ -0,0 +1,18 @@ +import { FunctionComponent } from "react"; + +import CircularProgress from "@mui/material/CircularProgress"; + +type Props = { + name: string; +}; + +const Loading: FunctionComponent = ({ name }) => { + return ( +
+ +

Loading {name}

+
+ ); +}; + +export default Loading; diff --git a/gui/src/localStyles.css b/gui/src/localStyles.css index 7f94274a..c6dc3067 100644 --- a/gui/src/localStyles.css +++ b/gui/src/localStyles.css @@ -173,7 +173,7 @@ span.EditorTitle { padding: 5px; } -.PlotLoader { +.Loading { padding: 10px; display: grid; place-items: center; diff --git a/gui/vite.config.ts b/gui/vite.config.ts index fef4d195..d536b91c 100644 --- a/gui/vite.config.ts +++ b/gui/vite.config.ts @@ -10,8 +10,14 @@ export default defineConfig({ rollupOptions: { output: { manualChunks: { - monaco: ["monaco-editor", "@monaco-editor/react"], - utilities: ["jszip", "@octokit/rest"], + utilities: [ + "jszip", + "@octokit/rest", + "react-dropzone", + "pyodide", + "webr", + ], + mui: ["@mui/material", "@mui/icons-material"], }, }, },