From 06facbdbb128d9dd8ebce840da8bc06c2d08bf48 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Thu, 14 Aug 2025 15:49:41 -0500 Subject: [PATCH 1/2] Spike: Lazy load Monaco (DH-20141) --- package-lock.json | 199 +++++++- .../code-studio/src/main/AppMainContainer.tsx | 26 +- .../src/settings/EditorSectionContent.tsx | 4 +- .../src/settings/LazySettingsMenu.tsx | 18 + .../code-studio/src/settings/SettingsMenu.tsx | 2 +- packages/console/src/ConsoleInput.tsx | 11 +- packages/console/src/common/Code.tsx | 2 +- packages/console/src/log/LogView.tsx | 15 +- .../src/monaco/LazyRuffSettingsModal.tsx | 25 + .../src/monaco/MonacoProviders.test.tsx | 2 +- .../console/src/monaco/MonacoProviders.tsx | 438 +++++++++--------- .../src/monaco/MonacoThemeProvider.tsx | 4 +- .../console/src/monaco/MonacoUtils.test.ts | 2 +- packages/console/src/monaco/MonacoUtils.ts | 164 ++++--- .../console/src/monaco/RuffSettingsModal.tsx | 37 +- packages/console/src/monaco/index.ts | 1 + packages/console/src/notebook/Editor.tsx | 18 +- .../console/src/notebook/ScriptEditor.tsx | 92 ++-- packages/embed-widget/package.json | 3 +- packages/embed-widget/vite.config.ts | 89 +++- .../iris-grid/src/sidebar/InputEditor.tsx | 14 +- 21 files changed, 792 insertions(+), 374 deletions(-) create mode 100644 packages/code-studio/src/settings/LazySettingsMenu.tsx create mode 100644 packages/console/src/monaco/LazyRuffSettingsModal.tsx diff --git a/package-lock.json b/package-lock.json index 0d10132f12..6604a7cee0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26338,6 +26338,124 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-visualizer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.3.tgz", + "integrity": "sha512-ZU41GwrkDcCpVoffviuM9Clwjy5fcUxlz0oMoTXTYsK+tcIFzbdacnrr2n8TXcHxbGKKXtOdjxM2HUS4HjkwIw==", + "dev": true, + "dependencies": { + "open": "^8.0.0", + "picomatch": "^4.0.2", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "rolldown": "1.x || ^1.0.0-beta", + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -30595,7 +30713,8 @@ "@deephaven/eslint-config": "file:../eslint-config", "@deephaven/mocks": "file:../mocks", "@deephaven/prettier-config": "file:../prettier-config", - "@deephaven/stylelint-config": "file:../stylelint-config" + "@deephaven/stylelint-config": "file:../stylelint-config", + "rollup-plugin-visualizer": "^6.0.3" } }, "packages/embed-widget/node_modules/@deephaven/jsapi-types": { @@ -33196,7 +33315,8 @@ "nanoid": "5.0.7", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-redux": "^7.2.4" + "react-redux": "^7.2.4", + "rollup-plugin-visualizer": "^6.0.3" }, "dependencies": { "@deephaven/jsapi-types": { @@ -51180,6 +51300,81 @@ "fsevents": "~2.3.2" } }, + "rollup-plugin-visualizer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.3.tgz", + "integrity": "sha512-ZU41GwrkDcCpVoffviuM9Clwjy5fcUxlz0oMoTXTYsK+tcIFzbdacnrr2n8TXcHxbGKKXtOdjxM2HUS4HjkwIw==", + "dev": true, + "requires": { + "open": "^8.0.0", + "picomatch": "^4.0.2", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true + }, + "source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", diff --git a/packages/code-studio/src/main/AppMainContainer.tsx b/packages/code-studio/src/main/AppMainContainer.tsx index a30fb05fdd..880bd6e78d 100644 --- a/packages/code-studio/src/main/AppMainContainer.tsx +++ b/packages/code-studio/src/main/AppMainContainer.tsx @@ -46,7 +46,7 @@ import { emitCycleToPreviousTab, } from '@deephaven/dashboard'; import { - ConsolePlugin, + // ConsolePlugin, InputFilterEvent, MarkdownEvent, NotebookEvent, @@ -93,7 +93,7 @@ import { createExportLogsContextAction, } from '@deephaven/app-utils'; import JSZip from 'jszip'; -import SettingsMenu from '../settings/SettingsMenu'; +import { LazySettingsMenu } from '../settings/LazySettingsMenu'; import AppControlsMenu from './AppControlsMenu'; import { getLayoutStorage, getServerConfigValues } from '../redux'; import './AppMainContainer.scss'; @@ -1053,21 +1053,21 @@ export class AppMainContainer extends Component< ) } plugins={[ - , + // , ...dashboardPlugins, ]} /> - {isRuffSettingsOpen && ( - import('./SettingsMenu')); + +export function LazySettingsMenu(props: SettingsMenuProps): JSX.Element { + return ( + } + > + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + + ); +} + +export default LazySettingsMenu; diff --git a/packages/code-studio/src/settings/SettingsMenu.tsx b/packages/code-studio/src/settings/SettingsMenu.tsx index 34f72a5ece..1f74e1ddcf 100644 --- a/packages/code-studio/src/settings/SettingsMenu.tsx +++ b/packages/code-studio/src/settings/SettingsMenu.tsx @@ -47,7 +47,7 @@ import AdvancedSectionContent from './AdvancedSectionContent'; import ThemeSectionContent from './ThemeSectionContent'; import EditorSectionContent from './EditorSectionContent'; -interface SettingsMenuProps { +export interface SettingsMenuProps { serverConfigValues: ServerConfigValues; pluginData: PluginModuleMap; user: User; diff --git a/packages/console/src/ConsoleInput.tsx b/packages/console/src/ConsoleInput.tsx index 5d6707a6d6..200038332c 100644 --- a/packages/console/src/ConsoleInput.tsx +++ b/packages/console/src/ConsoleInput.tsx @@ -1,6 +1,6 @@ import React, { PureComponent, type ReactElement, type RefObject } from 'react'; import classNames from 'classnames'; -import * as monaco from 'monaco-editor'; +import type * as Monaco from 'monaco-editor'; import Log from '@deephaven/log'; import { assertNotNull, @@ -38,7 +38,7 @@ interface ConsoleInputProps { interface ConsoleInputState { commandEditorHeight: number; isFocused: boolean; - model: monaco.editor.ITextModel | null; + model: Monaco.editor.ITextModel | null; } /** @@ -103,7 +103,7 @@ export class ConsoleInput extends PureComponent< commandContainer: RefObject; - commandEditor?: monaco.editor.IStandaloneCodeEditor; + commandEditor?: Monaco.editor.IStandaloneCodeEditor; commandHistoryIndex: number | null; @@ -156,12 +156,13 @@ export class ConsoleInput extends PureComponent< } } - initCommandEditor(): void { + async initCommandEditor(): Promise { const { language, session } = this.props; + const monaco = await MonacoUtils.lazyMonaco(); const model = monaco.editor.createModel( '', language, - MonacoUtils.generateConsoleUri() + await MonacoUtils.generateConsoleUri() ); const commandSettings = { copyWithSyntaxHighlighting: false, diff --git a/packages/console/src/common/Code.tsx b/packages/console/src/common/Code.tsx index 8786207884..33cddd9b69 100644 --- a/packages/console/src/common/Code.tsx +++ b/packages/console/src/common/Code.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState, type ReactNode } from 'react'; -import * as monaco from 'monaco-editor'; import { useTheme } from '@deephaven/components'; interface CodeProps { @@ -15,6 +14,7 @@ function Code({ children, language }: CodeProps): JSX.Element { let isCanceled = false; async function colorize() { if (children != null && activeThemes != null) { + const monaco = await import('monaco-editor'); const result = await monaco.editor.colorize( children.toString(), language, diff --git a/packages/console/src/log/LogView.tsx b/packages/console/src/log/LogView.tsx index ee7b79b25e..1936d2c374 100644 --- a/packages/console/src/log/LogView.tsx +++ b/packages/console/src/log/LogView.tsx @@ -8,7 +8,7 @@ import { vsGear, dhTrashUndo } from '@deephaven/icons'; import { assertNotNull } from '@deephaven/utils'; import type { dh } from '@deephaven/jsapi-types'; import { type Placement } from 'popper.js'; -import * as monaco from 'monaco-editor'; +import type * as Monaco from 'monaco-editor'; import ConsoleUtils from '../common/ConsoleUtils'; import LogLevel from './LogLevel'; import './LogView.scss'; @@ -84,10 +84,10 @@ class LogView extends PureComponent { componentDidMount(): void { this.resetLogLevels(); - this.initMonaco(); - this.startListening(); - - window.addEventListener('resize', this.handleResize); + this.initMonaco().then(() => { + this.startListening(); + window.addEventListener('resize', this.handleResize); + }); } componentDidUpdate(prevProps: LogViewProps, prevState: LogViewState): void { @@ -117,7 +117,7 @@ class LogView extends PureComponent { cancelListener?: () => void | null; - editor?: monaco.editor.IStandaloneCodeEditor; + editor?: Monaco.editor.IStandaloneCodeEditor; editorContainer: HTMLDivElement | null; @@ -211,7 +211,8 @@ class LogView extends PureComponent { } } - initMonaco(): void { + async initMonaco(): Promise { + const monaco = await MonacoUtils.lazyMonaco(); assertNotNull(this.editorContainer); this.editor = monaco.editor.create(this.editorContainer, { copyWithSyntaxHighlighting: false, diff --git a/packages/console/src/monaco/LazyRuffSettingsModal.tsx b/packages/console/src/monaco/LazyRuffSettingsModal.tsx new file mode 100644 index 0000000000..c60a19172a --- /dev/null +++ b/packages/console/src/monaco/LazyRuffSettingsModal.tsx @@ -0,0 +1,25 @@ +import React, { lazy, Suspense } from 'react'; +import { usePromiseFactory } from '@deephaven/react-hooks'; +import type { RuffSettingsModalProps } from './RuffSettingsModal'; +import MonacoUtils from './MonacoUtils'; + +const RuffSettingsModal = lazy(() => import('./RuffSettingsModal')); + +export function LazyRuffSettingsModal( + props: RuffSettingsModalProps +): JSX.Element | null { + const { data: monaco } = usePromiseFactory(MonacoUtils.lazyMonaco, []); + + if (monaco == null) { + return null; + } + + return ( + }> + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + + ); +} + +export default LazyRuffSettingsModal; diff --git a/packages/console/src/monaco/MonacoProviders.test.tsx b/packages/console/src/monaco/MonacoProviders.test.tsx index 80f10d19d2..177115f187 100644 --- a/packages/console/src/monaco/MonacoProviders.test.tsx +++ b/packages/console/src/monaco/MonacoProviders.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render } from '@testing-library/react'; -import * as monaco from 'monaco-editor'; +// import * as monaco from 'monaco-editor'; import dh from '@deephaven/jsapi-shim'; import type { DocumentRange, Position } from '@deephaven/jsapi-types'; import MonacoProviders from './MonacoProviders'; diff --git a/packages/console/src/monaco/MonacoProviders.tsx b/packages/console/src/monaco/MonacoProviders.tsx index 54cee45677..d0edcc3b57 100644 --- a/packages/console/src/monaco/MonacoProviders.tsx +++ b/packages/console/src/monaco/MonacoProviders.tsx @@ -2,7 +2,7 @@ * Completion provider for a code session */ import { PureComponent } from 'react'; -import * as monaco from 'monaco-editor'; +import type * as Monaco from 'monaco-editor'; import Log from '@deephaven/log'; import type { dh } from '@deephaven/jsapi-types'; import init, { Workspace, type Diagnostic } from '@astral-sh/ruff-wasm-web'; @@ -12,7 +12,7 @@ import MonacoUtils from './MonacoUtils'; const log = Log.module('MonacoCompletionProvider'); interface MonacoProviderProps { - model: monaco.editor.ITextModel; + model: Monaco.editor.ITextModel; session: dh.IdeSession; language: string; } @@ -46,10 +46,10 @@ class MonacoProviders extends PureComponent< return MonacoProviders.initRuffPromise; } - MonacoProviders.initRuffPromise = init({}).then(() => { + MonacoProviders.initRuffPromise = init({}).then(async () => { log.debug('Initialized Ruff', Workspace.version()); MonacoProviders.isRuffInitialized = true; - MonacoProviders.updateRuffWorkspace(); + await MonacoProviders.updateRuffWorkspace(); }); return MonacoProviders.initRuffPromise; @@ -59,7 +59,7 @@ class MonacoProviders extends PureComponent< * Updates the current ruff workspace with MonacoProviders.ruffSettings. * Re-lints all Python models after updating. */ - static updateRuffWorkspace(): void { + static async updateRuffWorkspace(): Promise { if (!MonacoProviders.isRuffInitialized) { return; } @@ -76,7 +76,7 @@ class MonacoProviders extends PureComponent< } /* eslint-enable no-console */ - MonacoProviders.lintAllPython(); + await MonacoProviders.lintAllPython(); } /** @@ -96,7 +96,7 @@ class MonacoProviders extends PureComponent< MonacoProviders.updateRuffWorkspace(); } - static getDiagnostics(model: monaco.editor.ITextModel): Diagnostic[] { + static getDiagnostics(model: Monaco.editor.ITextModel): Diagnostic[] { if (!MonacoProviders.ruffWorkspace) { return []; } @@ -111,7 +111,9 @@ class MonacoProviders extends PureComponent< return diagnostics; } - static lintAllPython(): void { + static async lintAllPython(): Promise { + const monaco = await MonacoUtils.lazyMonaco(); + if (!MonacoProviders.isRuffEnabled) { monaco.editor.removeAllMarkers('ruff'); return; @@ -123,7 +125,7 @@ class MonacoProviders extends PureComponent< .forEach(MonacoProviders.lintPython); } - static lintPython(model: monaco.editor.ITextModel): void { + static async lintPython(model: Monaco.editor.ITextModel): Promise { if (!MonacoProviders.isRuffEnabled) { return; } @@ -135,6 +137,8 @@ class MonacoProviders extends PureComponent< const diagnostics = MonacoProviders.getDiagnostics(model); log.debug(`Linting Python document: ${model.uri.toString()}`, diagnostics); + const monaco = await MonacoUtils.lazyMonaco(); + monaco.editor.setModelMarkers( model, 'ruff', @@ -165,7 +169,8 @@ class MonacoProviders extends PureComponent< * @param kind The LSP kind * @returns Monaco kind */ - static lspToMonacoKind(kind: number | undefined): number { + static async lspToMonacoKind(kind: number | undefined): Promise { + const monaco = await MonacoUtils.lazyMonaco(); const monacoKinds = monaco.languages.CompletionItemKind; switch (kind) { case 1: @@ -230,7 +235,7 @@ class MonacoProviders extends PureComponent< * @param range The LSP document range to convert * @returns The corresponding monaco range */ - static lspToMonacoRange(range: dh.lsp.Range): monaco.IRange { + static lspToMonacoRange(range: dh.lsp.Range): Monaco.IRange { const { start, end } = range; // Monaco expects the columns/ranges to start at 1. LSP starts at 0 @@ -250,7 +255,7 @@ class MonacoProviders extends PureComponent< * @returns The corresponding LSP position */ static monacoToLspPosition( - position: monaco.IPosition + position: Monaco.IPosition ): Pick { // Monaco 1-indexes Position. LSP 0-indexes Position return { @@ -259,175 +264,188 @@ class MonacoProviders extends PureComponent< }; } - static handlePythonCodeActionRequest( - model: monaco.editor.ITextModel, - range: monaco.Range - ): monaco.languages.ProviderResult { - if (!MonacoProviders.isRuffEnabled || !MonacoProviders.ruffWorkspace) { - return { - actions: [], - dispose: () => { - /* no-op */ - }, - }; - } - - const diagnostics = MonacoProviders.getDiagnostics(model).filter(d => { - const diagnosticRange = new monaco.Range( - d.location.row, - d.location.column, - d.end_location.row, - d.end_location.column - ); - return ( - d.code != null && // Syntax errors have no code and can't be fixed/disabled - diagnosticRange.intersectRanges(range) - ); - }); - - const fixActions: monaco.languages.CodeAction[] = diagnostics - .filter(({ fix }) => fix != null) - .map(d => { - let title = 'Fix'; - if (d.fix != null) { - if (d.fix.message != null && d.fix.message !== '') { - title = `${d.code}: ${d.fix.message}`; - } else { - title = `Fix ${d.code}`; - } - } + static async createPythonCodeActionRequestHandler(): Promise< + ( + model: Monaco.editor.ITextModel, + range: Monaco.Range + ) => Monaco.languages.ProviderResult + > { + const monaco = await MonacoUtils.lazyMonaco(); + + return function handlePythonCodeActionRequest( + model: Monaco.editor.ITextModel, + range: Monaco.Range + ): Monaco.languages.ProviderResult { + if (!MonacoProviders.isRuffEnabled || !MonacoProviders.ruffWorkspace) { return { - title, - id: `fix-${d.code}`, - kind: 'quickfix', - edit: d.fix - ? { - edits: d.fix.edits.map(edit => ({ - resource: model.uri, - versionId: model.getVersionId(), - textEdit: { - range: { - startLineNumber: edit.location.row, - startColumn: edit.location.column, - endLineNumber: edit.end_location.row, - endColumn: edit.end_location.column, - }, - text: edit.content ?? '', - }, - })), - } - : undefined, + actions: [], + dispose: () => { + /* no-op */ + }, }; + } + + const diagnostics = MonacoProviders.getDiagnostics(model).filter(d => { + const diagnosticRange = new monaco.Range( + d.location.row, + d.location.column, + d.end_location.row, + d.end_location.column + ); + return ( + d.code != null && // Syntax errors have no code and can't be fixed/disabled + diagnosticRange.intersectRanges(range) + ); }); - const seenCodes = new Set(); - const duplicateCodes = new Set(); - diagnostics.forEach(d => { - if (d.code == null) { - return; - } - if (seenCodes.has(d.code)) { - duplicateCodes.add(d.code); - } - seenCodes.add(d.code); - }); + const fixActions: Monaco.languages.CodeAction[] = diagnostics + .filter(({ fix }) => fix != null) + .map(d => { + let title = 'Fix'; + if (d.fix != null) { + if (d.fix.message != null && d.fix.message !== '') { + title = `${d.code}: ${d.fix.message}`; + } else { + title = `Fix ${d.code}`; + } + } + return { + title, + id: `fix-${d.code}`, + kind: 'quickfix', + edit: d.fix + ? { + edits: d.fix.edits.map(edit => ({ + resource: model.uri, + versionId: model.getVersionId(), + textEdit: { + range: { + startLineNumber: edit.location.row, + startColumn: edit.location.column, + endLineNumber: edit.end_location.row, + endColumn: edit.end_location.column, + }, + text: edit.content ?? '', + }, + })), + } + : undefined, + }; + }); - const disableLineActions: monaco.languages.CodeAction[] = diagnostics - .map(d => { + const seenCodes = new Set(); + const duplicateCodes = new Set(); + diagnostics.forEach(d => { if (d.code == null) { - // The nulls are already filtered out, but TS doesn't know that - return []; + return; } - const line = model.getLineContent(d.location.row); - const lastToken = monaco.editor - .tokenize(line, model.getLanguageId())[0] - .at(-1); - const lineEdit = { - range: { - startLineNumber: d.location.row, - startColumn: line.length + 1, - endLineNumber: d.location.row, - endColumn: line.length + 1, - }, - text: ` # noqa: ${d.code}`, - }; - if (lastToken != null && lastToken.type.startsWith('comment')) { - // Already a comment at the end of the line - lineEdit.text = `# noqa: ${d.code} `; - if (line.startsWith('# noqa:', lastToken.offset)) { - // Already another suppressed rule on the line - lineEdit.range.startColumn = lastToken.offset + 1; - lineEdit.range.endColumn = lastToken.offset + 9; // "# noqa: " length + 1 to offset - } else { - lineEdit.range.startColumn = lastToken.offset + 1; - lineEdit.range.endColumn = line.startsWith('# ', lastToken.offset) - ? lastToken.offset + 3 // "# " + 1 to offset - : lastToken.offset + 2; // "#" + 1 to offset - } + if (seenCodes.has(d.code)) { + duplicateCodes.add(d.code); } - return [ - { - title: `Disable ${d.code} for ${ - duplicateCodes.has(d.code) - ? `line ${d.location.row}` - : 'this line' - }`, - kind: 'quickfix', - edit: { - edits: [ - { - resource: model.uri, - versionId: model.getVersionId(), - textEdit: lineEdit, - }, - ], - }, - }, - ]; - }) - .flat() - .filter( - // Remove actions with duplicate titles as you can't disable the same rule on a line twice - (action, i, arr) => arr.find(a => a.title === action.title) === action - ); + seenCodes.add(d.code); + }); - const disableGlobalActions: monaco.languages.CodeAction[] = [ - ...seenCodes, - ].map(code => ({ - title: `Disable ${code} for this file`, - kind: 'quickfix', - edit: { - edits: [ - { - resource: model.uri, - versionId: model.getVersionId(), - textEdit: { - range: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: 1, + const disableLineActions: Monaco.languages.CodeAction[] = diagnostics + .map(d => { + if (d.code == null) { + // The nulls are already filtered out, but TS doesn't know that + return []; + } + const line = model.getLineContent(d.location.row); + const lastToken = monaco.editor + .tokenize(line, model.getLanguageId())[0] + .at(-1); + const lineEdit = { + range: { + startLineNumber: d.location.row, + startColumn: line.length + 1, + endLineNumber: d.location.row, + endColumn: line.length + 1, + }, + text: ` # noqa: ${d.code}`, + }; + if (lastToken != null && lastToken.type.startsWith('comment')) { + // Already a comment at the end of the line + lineEdit.text = `# noqa: ${d.code} `; + if (line.startsWith('# noqa:', lastToken.offset)) { + // Already another suppressed rule on the line + lineEdit.range.startColumn = lastToken.offset + 1; + lineEdit.range.endColumn = lastToken.offset + 9; // "# noqa: " length + 1 to offset + } else { + lineEdit.range.startColumn = lastToken.offset + 1; + lineEdit.range.endColumn = line.startsWith('# ', lastToken.offset) + ? lastToken.offset + 3 // "# " + 1 to offset + : lastToken.offset + 2; // "#" + 1 to offset + } + } + return [ + { + title: `Disable ${d.code} for ${ + duplicateCodes.has(d.code) + ? `line ${d.location.row}` + : 'this line' + }`, + kind: 'quickfix', + edit: { + edits: [ + { + resource: model.uri, + versionId: model.getVersionId(), + textEdit: lineEdit, + }, + ], }, - text: `# ruff: noqa: ${code}\n`, }, - }, - ], - }, - })); + ]; + }) + .flat() + .filter( + // Remove actions with duplicate titles as you can't disable the same rule on a line twice + (action, i, arr) => arr.find(a => a.title === action.title) === action + ); + + const disableGlobalActions: Monaco.languages.CodeAction[] = [ + ...seenCodes, + ].map(code => ({ + title: `Disable ${code} for this file`, + kind: 'quickfix', + edit: { + edits: [ + { + resource: model.uri, + versionId: model.getVersionId(), + textEdit: { + range: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 1, + }, + text: `# ruff: noqa: ${code}\n`, + }, + }, + ], + }, + })); - return { - actions: [...fixActions, ...disableLineActions, ...disableGlobalActions], - dispose: () => { - /* no-op */ - }, + return { + actions: [ + ...fixActions, + ...disableLineActions, + ...disableGlobalActions, + ], + dispose: () => { + /* no-op */ + }, + }; }; } static handlePythonFormatRequest( - model: monaco.editor.ITextModel, - options: monaco.languages.FormattingOptions, - token: monaco.CancellationToken - ): monaco.languages.ProviderResult { + model: Monaco.editor.ITextModel, + options: Monaco.languages.FormattingOptions, + token: Monaco.CancellationToken + ): Monaco.languages.ProviderResult { if (!MonacoProviders.ruffWorkspace) { return; } @@ -453,28 +471,30 @@ class MonacoProviders extends PureComponent< componentDidMount(): void { const { language, session } = this.props; - this.registeredCompletionProvider = - monaco.languages.registerCompletionItemProvider(language, { - provideCompletionItems: this.handleCompletionRequest, - triggerCharacters: ['.', '"', "'"], - }); - - if (session.getSignatureHelp != null) { - this.registeredSignatureProvider = - monaco.languages.registerSignatureHelpProvider(language, { - provideSignatureHelp: this.handleSignatureRequest, - signatureHelpTriggerCharacters: ['(', ','], + MonacoUtils.lazyMonaco().then(monaco => { + this.registeredCompletionProvider = + monaco.languages.registerCompletionItemProvider(language, { + provideCompletionItems: this.handleCompletionRequest, + triggerCharacters: ['.', '"', "'"], }); - } - if (session.getHover != null) { - this.registeredHoverProvider = monaco.languages.registerHoverProvider( - language, - { - provideHover: this.handleHoverRequest, - } - ); - } + if (session.getSignatureHelp != null) { + this.registeredSignatureProvider = + monaco.languages.registerSignatureHelpProvider(language, { + provideSignatureHelp: this.handleSignatureRequest, + signatureHelpTriggerCharacters: ['(', ','], + }); + } + + if (session.getHover != null) { + this.registeredHoverProvider = monaco.languages.registerHoverProvider( + language, + { + provideHover: this.handleHoverRequest, + } + ); + } + }); } componentWillUnmount(): void { @@ -483,17 +503,17 @@ class MonacoProviders extends PureComponent< this.registeredHoverProvider?.dispose(); } - registeredCompletionProvider?: monaco.IDisposable; + registeredCompletionProvider?: Monaco.IDisposable; - registeredSignatureProvider?: monaco.IDisposable; + registeredSignatureProvider?: Monaco.IDisposable; - registeredHoverProvider?: monaco.IDisposable; + registeredHoverProvider?: Monaco.IDisposable; handleCompletionRequest( - model: monaco.editor.ITextModel, - position: monaco.Position, - context: monaco.languages.CompletionContext - ): monaco.languages.ProviderResult { + model: Monaco.editor.ITextModel, + position: Monaco.Position, + context: Monaco.languages.CompletionContext + ): Monaco.languages.ProviderResult { const { model: propModel, session } = this.props; if (model !== propModel) { return null; @@ -512,10 +532,13 @@ class MonacoProviders extends PureComponent< log.debug('Requested completion items', params); const monacoCompletionItems = completionItems - .then(items => { + .then(async items => { log.debug('Completion items received: ', params, items); - const suggestions = items.map(item => { + const suggestions: Monaco.languages.CompletionItem[] = []; + + // eslint-disable-next-line no-restricted-syntax + for (const item of items) { const { label, kind, @@ -527,9 +550,10 @@ class MonacoProviders extends PureComponent< insertTextFormat, } = item; - return { + suggestions.push({ label, - kind: MonacoProviders.lspToMonacoKind(kind), + // eslint-disable-next-line no-await-in-loop + kind: await MonacoProviders.lspToMonacoKind(kind), detail, documentation: documentation?.kind === 'markdown' @@ -543,8 +567,8 @@ class MonacoProviders extends PureComponent< // Why microsoft is using almost-but-not-LSP apis is beyond me.... insertTextRules: insertTextFormat === 2 ? 4 : insertTextFormat, range: MonacoProviders.lspToMonacoRange(textEdit.range), - }; - }); + }); + } return { incomplete: true, @@ -560,12 +584,12 @@ class MonacoProviders extends PureComponent< } handleSignatureRequest( - model: monaco.editor.ITextModel, - position: monaco.Position, - token: monaco.CancellationToken, - context: monaco.languages.SignatureHelpContext - ): monaco.languages.ProviderResult { - const defaultResult: monaco.languages.SignatureHelpResult = { + model: Monaco.editor.ITextModel, + position: Monaco.Position, + token: Monaco.CancellationToken, + context: Monaco.languages.SignatureHelpContext + ): Monaco.languages.ProviderResult { + const defaultResult: Monaco.languages.SignatureHelpResult = { value: { signatures: [], activeSignature: 0, @@ -637,9 +661,9 @@ class MonacoProviders extends PureComponent< } handleHoverRequest( - model: monaco.editor.ITextModel, - position: monaco.Position - ): monaco.languages.ProviderResult { + model: Monaco.editor.ITextModel, + position: Monaco.Position + ): Monaco.languages.ProviderResult { const { model: propModel, session } = this.props; if (model !== propModel || session.getHover == null) { return null; diff --git a/packages/console/src/monaco/MonacoThemeProvider.tsx b/packages/console/src/monaco/MonacoThemeProvider.tsx index 27c2dd1baf..6020f34106 100644 --- a/packages/console/src/monaco/MonacoThemeProvider.tsx +++ b/packages/console/src/monaco/MonacoThemeProvider.tsx @@ -1,6 +1,6 @@ import { type ReactNode, useEffect } from 'react'; import { useTheme } from '@deephaven/components'; -import MonacoUtils from './MonacoUtils'; +// import MonacoUtils from './MonacoUtils'; export interface MonacoThemeProviderProps { children: ReactNode; @@ -14,7 +14,7 @@ export function MonacoThemeProvider({ useEffect( function refreshMonacoTheme() { if (activeThemes != null) { - MonacoUtils.initTheme(); + // MonacoUtils.initTheme(); } }, [activeThemes] diff --git a/packages/console/src/monaco/MonacoUtils.test.ts b/packages/console/src/monaco/MonacoUtils.test.ts index 915334197c..2325729fef 100644 --- a/packages/console/src/monaco/MonacoUtils.test.ts +++ b/packages/console/src/monaco/MonacoUtils.test.ts @@ -1,5 +1,5 @@ /* eslint-disable no-bitwise */ -import * as monaco from 'monaco-editor'; +// import * as monaco from 'monaco-editor'; import { Shortcut, KEY, MODIFIER } from '@deephaven/components'; import { TestUtils } from '@deephaven/test-utils'; import MonacoUtils from './MonacoUtils'; diff --git a/packages/console/src/monaco/MonacoUtils.ts b/packages/console/src/monaco/MonacoUtils.ts index 3ee5b00d3a..b2bff52065 100644 --- a/packages/console/src/monaco/MonacoUtils.ts +++ b/packages/console/src/monaco/MonacoUtils.ts @@ -11,12 +11,8 @@ import { import type { dh } from '@deephaven/jsapi-types'; import { assertNotNull } from '@deephaven/utils'; import { find as linkifyFind } from 'linkifyjs'; -import * as monaco from 'monaco-editor'; -import type { Environment } from 'monaco-editor'; -// @ts-ignore -import { KeyCodeUtils } from 'monaco-editor/esm/vs/base/common/keyCodes.js'; +import type * as Monaco from 'monaco-editor'; import Log from '@deephaven/log'; -import MonacoThemeRaw from './MonacoTheme.module.scss'; import PyLang from './lang/python'; import GroovyLang from './lang/groovy'; import ScalaLang from './lang/scala'; @@ -24,6 +20,7 @@ import DbLang from './lang/db'; import LogLang from './lang/log'; import { type Language } from './lang/Language'; import MonacoProviders from './MonacoProviders'; +import MonacoThemeRaw from './MonacoTheme.module.scss'; const log = Log.module('MonacoUtils'); @@ -31,19 +28,28 @@ const CONSOLE_URI_PREFIX = 'inmemory://dh-console/'; declare global { interface Window { - MonacoEnvironment?: Environment; + MonacoEnvironment?: Monaco.Environment; } } class MonacoUtils { + /** + * Lazy load the monaco module + * @returns Promise to the monaco module + */ + static async lazyMonaco(): Promise { + log.debug('Lazy loading Monaco...'); + return import('monaco-editor'); + } + /** * Initializes Monaco for the environment * @param getWorker The getWorker function Monaco should use * The workers should be provided by the caller and bundled by their build system (e.g. Vite, Webpack) */ - static init({ + static async init({ getWorker, - }: { getWorker?: Environment['getWorker'] } = {}): void { + }: { getWorker?: Monaco.Environment['getWorker'] } = {}): Promise { log.debug('Initializing Monaco...'); if (getWorker !== undefined) { @@ -52,15 +58,18 @@ class MonacoUtils { const { initTheme, registerLanguages } = MonacoUtils; - initTheme(); + await initTheme(); + + await registerLanguages([DbLang, PyLang, GroovyLang, LogLang, ScalaLang]); - registerLanguages([DbLang, PyLang, GroovyLang, LogLang, ScalaLang]); + const monaco = await MonacoUtils.lazyMonaco(); - monaco.languages.onLanguage('python', () => { + monaco.languages.onLanguage('python', async () => { monaco.languages.registerCodeActionProvider( 'python', { - provideCodeActions: MonacoProviders.handlePythonCodeActionRequest, + provideCodeActions: + await MonacoProviders.createPythonCodeActionRequestHandler(), }, { providedCodeActionKinds: ['quickfix'] } ); @@ -71,15 +80,15 @@ class MonacoUtils { }); }); - monaco.editor.onDidCreateModel(model => { + monaco.editor.onDidCreateModel(async model => { // Lint Python models on creation and on change if (model.getLanguageId() === 'python') { if (MonacoProviders.ruffWorkspace != null) { - MonacoProviders.lintPython(model); + await MonacoProviders.lintPython(model); } const throttledLint = throttle( - (m: monaco.editor.ITextModel) => MonacoProviders.lintPython(m), + (m: Monaco.editor.ITextModel) => MonacoProviders.lintPython(m), 250 ); @@ -89,7 +98,7 @@ class MonacoUtils { } }); - MonacoUtils.removeConflictingKeybindings(); + await MonacoUtils.removeConflictingKeybindings(); log.debug('Monaco initialized.'); } @@ -97,9 +106,11 @@ class MonacoUtils { /** * Initialize current Monaco theme based on the current DH theme. */ - static initTheme(): void { + static async initTheme(): Promise { const { removeHashtag } = MonacoUtils; + const monaco = await MonacoUtils.lazyMonaco(); + const MonacoTheme = resolveCssVariablesInRecord(MonacoThemeRaw); log.debug2('Monaco theme:', MonacoThemeRaw); log.debug2('Monaco theme derived:', MonacoTheme); @@ -263,7 +274,7 @@ class MonacoUtils { * Register the getWorker function for Monaco * @param getWorker The getWorker function for Monaco */ - static registerGetWorker(getWorker: Environment['getWorker']): void { + static registerGetWorker(getWorker: Monaco.Environment['getWorker']): void { window.MonacoEnvironment = { ...window.MonacoEnvironment, getWorker, @@ -279,7 +290,9 @@ class MonacoUtils { return color?.substring(1) ?? ''; } - static registerLanguages(languages: Language[]): void { + static async registerLanguages(languages: Language[]): Promise { + const monaco = await MonacoUtils.lazyMonaco(); + // First override the default loader for any language we have a custom definition for // https://github.com/Microsoft/monaco-editor/issues/252#issuecomment-482786867 const languageIds = languages.map(({ id }) => id); @@ -292,13 +305,17 @@ class MonacoUtils { }); // Then register our language definitions - languages.forEach(language => { - MonacoUtils.registerLanguage(language); - }); + // eslint-disable-next-line no-restricted-syntax + for (const language of languages) { + // eslint-disable-next-line no-await-in-loop + await MonacoUtils.registerLanguage(language); + } } - static registerLanguage(language: Language): void { + static async registerLanguage(language: Language): Promise { log.debug2('Registering language: ', language.id); + + const monaco = await MonacoUtils.lazyMonaco(); monaco.languages.register(language); monaco.languages.onLanguage(language.id, () => { @@ -312,10 +329,16 @@ class MonacoUtils { * @param editor The editor to set the EOL for * @param eolSequence EOL sequence */ - static setEOL( - editor: monaco.editor.IStandaloneCodeEditor, - eolSequence = monaco.editor.EndOfLineSequence.LF - ): void { + static async setEOL( + editor: Monaco.editor.IStandaloneCodeEditor, + eolSequence?: Monaco.editor.EndOfLineSequence + ): Promise { + if (eolSequence == null) { + // eslint-disable-next-line no-param-reassign + eolSequence = (await MonacoUtils.lazyMonaco()).editor.EndOfLineSequence + .LF; + } + editor.getModel()?.setEOL(eolSequence); } @@ -326,9 +349,9 @@ class MonacoUtils { * @returns A cleanup function for disposing of the created listeners */ static openDocument( - editor: monaco.editor.IStandaloneCodeEditor, + editor: Monaco.editor.IStandaloneCodeEditor, session: dh.IdeSession - ): monaco.IDisposable { + ): Monaco.IDisposable { const model = editor.getModel(); assertNotNull(model); const didOpenDocumentParams = { @@ -385,7 +408,7 @@ class MonacoUtils { } static closeDocument( - editor: monaco.editor.IStandaloneCodeEditor, + editor: Monaco.editor.IStandaloneCodeEditor, session: dh.IdeSession ): void { const model = editor.getModel(); @@ -405,7 +428,7 @@ class MonacoUtils { * @param editor The editor the register the paste handler for */ static registerPasteHandler( - editor: monaco.editor.IStandaloneCodeEditor + editor: Monaco.editor.IStandaloneCodeEditor ): void { editor.onDidPaste(pasteEvent => { const smartQuotes = /“|”/g; @@ -443,7 +466,9 @@ class MonacoUtils { * them. Note that this is a global configuration, so all editor instances will * be impacted. */ - static removeConflictingKeybindings(): void { + static async removeConflictingKeybindings(): Promise { + const monaco = await MonacoUtils.lazyMonaco(); + // All editor instances share a global keybinding registry which is where // default keybindings are set. There doesn't appear to be a way to remove // default bindings, but we can add new ones that will override the existing @@ -479,7 +504,7 @@ class MonacoUtils { * combination like `monaco.KeyMod.Alt | monaco.KeyMod.KeyJ` */ static disableKeyBindings( - editor: monaco.editor.IStandaloneCodeEditor, + editor: Monaco.editor.IStandaloneCodeEditor, keybindings: number[] ): void { editor.addAction({ @@ -491,13 +516,21 @@ class MonacoUtils { }); } - static getMonacoKeyCodeFromShortcut(shortcut: Shortcut): number { + static async getMonacoKeyCodeFromShortcut( + shortcut: Shortcut + ): Promise { const { keyState } = shortcut; const { keyValue } = keyState; if (keyValue === null) { return 0; } + const monaco = await MonacoUtils.lazyMonaco(); + const KeyCodeUtils = await import( + // @ts-ignore + 'monaco-editor/esm/vs/base/common/keyCodes.js' + ); + const isMac = MonacoUtils.isMacPlatform(); if (isMac) { @@ -521,32 +554,40 @@ class MonacoUtils { ); } - static provideLinks(model: monaco.editor.ITextModel): { - links: monaco.languages.ILink[]; - } { - const newTokens: monaco.languages.ILink[] = []; - - for (let i = 1; i <= model.getLineCount(); i += 1) { - const lineText = model.getLineContent(i); - const originalTokens = linkifyFind(lineText); + static async createProvideLinks(): Promise< + Monaco.languages.LinkProvider['provideLinks'] + > { + const monaco = await MonacoUtils.lazyMonaco(); - const tokens = originalTokens.filter(token => { - if (token.type === 'url') { - return /^https?:\/\//.test(token.value); - } - return true; - }); - // map the tokens to the ranges - you know the line number now, use the token start/end as the startColumn/endColumn - tokens.forEach(token => { - newTokens.push({ - url: token.href, - range: new monaco.Range(i, token.start + 1, i, token.end + 1), + return ( + model: Monaco.editor.ITextModel + ): { + links: Monaco.languages.ILink[]; + } => { + const newTokens: Monaco.languages.ILink[] = []; + + for (let i = 1; i <= model.getLineCount(); i += 1) { + const lineText = model.getLineContent(i); + const originalTokens = linkifyFind(lineText); + + const tokens = originalTokens.filter(token => { + if (token.type === 'url') { + return /^https?:\/\//.test(token.value); + } + return true; }); - }); - } + // map the tokens to the ranges - you know the line number now, use the token start/end as the startColumn/endColumn + tokens.forEach(token => { + newTokens.push({ + url: token.href, + range: new monaco.Range(i, token.start + 1, i, token.end + 1), + }); + }); + } - return { - links: newTokens, + return { + links: newTokens, + }; }; } @@ -554,7 +595,8 @@ class MonacoUtils { * Generates a console URI for use with monaco. * @returns A new console URI */ - static generateConsoleUri(): monaco.Uri { + static async generateConsoleUri(): Promise { + const monaco = await MonacoUtils.lazyMonaco(); return monaco.Uri.parse(`${CONSOLE_URI_PREFIX}${nanoid()}`); } @@ -563,7 +605,7 @@ class MonacoUtils { * @param model The monaco model to check * @returns If the model is a console model */ - static isConsoleModel(model: monaco.editor.ITextModel): boolean { + static isConsoleModel(model: Monaco.editor.ITextModel): boolean { return model.uri.toString().startsWith(CONSOLE_URI_PREFIX); } @@ -572,7 +614,7 @@ class MonacoUtils { * @param editor The monaco editor to check * @returns If the editor has a document formatter registered */ - static canFormat(editor: monaco.editor.IStandaloneCodeEditor): boolean { + static canFormat(editor: Monaco.editor.IStandaloneCodeEditor): boolean { return ( editor.getAction('editor.action.formatDocument')?.isSupported() === true ); @@ -583,7 +625,7 @@ class MonacoUtils { * @param editor The editor to format */ static async formatDocument( - editor: monaco.editor.IStandaloneCodeEditor + editor: Monaco.editor.IStandaloneCodeEditor ): Promise { await editor.getAction('editor.action.formatDocument')?.run(); } diff --git a/packages/console/src/monaco/RuffSettingsModal.tsx b/packages/console/src/monaco/RuffSettingsModal.tsx index d09e02f5ca..dbdda2ce89 100644 --- a/packages/console/src/monaco/RuffSettingsModal.tsx +++ b/packages/console/src/monaco/RuffSettingsModal.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; -import * as monaco from 'monaco-editor'; +import type * as Monaco from 'monaco-editor'; import { Workspace } from '@astral-sh/ruff-wasm-web'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { @@ -25,7 +25,7 @@ import ruffSchema from './ruffSchema'; import './RuffSettingsModal.scss'; import MonacoProviders from './MonacoProviders'; -interface RuffSettingsModalProps { +export interface RuffSettingsModalProps { text: string; isOpen: boolean; onClose: () => void; @@ -34,11 +34,14 @@ interface RuffSettingsModalProps { defaultSettings?: Record; } -const RUFF_SETTINGS_URI = monaco.Uri.parse( - 'inmemory://dh-config/ruff-settings.json' -); +// const RUFF_SETTINGS_URI = Monaco.Uri.parse( +// 'inmemory://dh-config/ruff-settings.json' +// ); -function registerRuffSchema(): void { +function registerRuffSchema( + monaco: typeof Monaco, + ruffSettingsUri: Monaco.Uri +): void { const { schemas = [] } = monaco.languages.json.jsonDefaults.diagnosticsOptions; @@ -49,7 +52,7 @@ function registerRuffSchema(): void { ...schemas, { uri: 'json://ruff-schema', - fileMatch: [RUFF_SETTINGS_URI.toString()], + fileMatch: [ruffSettingsUri.toString()], schema: ruffSchema, }, ], @@ -65,14 +68,17 @@ async function getRuffVersion(): Promise { export default function RuffSettingsModal({ text, isOpen, + monaco, onClose, onSave, readOnly = false, defaultSettings = RUFF_DEFAULT_SETTINGS, -}: RuffSettingsModalProps): React.ReactElement | null { +}: RuffSettingsModalProps & { + monaco: typeof Monaco; +}): React.ReactElement | null { const [isValid, setIsValid] = useState(false); const [isDefault, setIsDefault] = useState(false); - const editorRef = useRef(); + const editorRef = useRef(); const formattedDefaultSettings = useMemo( () => JSON.stringify(defaultSettings, null, 2), @@ -81,8 +87,13 @@ export default function RuffSettingsModal({ const { data: ruffVersion } = usePromiseFactory(getRuffVersion); + const ruffSettingsUri = useMemo( + () => monaco.Uri.parse('inmemory://dh-config/ruff-settings.json'), + [monaco] + ); + const [model] = useState(() => - monaco.editor.createModel(text, 'json', RUFF_SETTINGS_URI) + monaco.editor.createModel(text, 'json', ruffSettingsUri) ); const handleClose = useCallback((): void => { @@ -129,17 +140,17 @@ export default function RuffSettingsModal({ }); const onEditorInitialized = useCallback( - (editor: monaco.editor.IStandaloneCodeEditor): void => { + (editor: Monaco.editor.IStandaloneCodeEditor): void => { editorRef.current = editor; model.onDidChangeContent(() => { debouncedValidate(model.getValue()); }); - registerRuffSchema(); + registerRuffSchema(monaco, ruffSettingsUri); debouncedValidate(model.getValue()); }, - [debouncedValidate, model] + [debouncedValidate, model, monaco, ruffSettingsUri] ); if (!isOpen) { diff --git a/packages/console/src/monaco/index.ts b/packages/console/src/monaco/index.ts index 3402c7077f..d0cee9cc67 100644 --- a/packages/console/src/monaco/index.ts +++ b/packages/console/src/monaco/index.ts @@ -3,4 +3,5 @@ export { default as MonacoProviders } from './MonacoProviders'; export { default as MonacoTheme } from './MonacoTheme.module.scss'; export * from './MonacoThemeProvider'; export { default as RuffSettingsModal } from './RuffSettingsModal'; +export { LazyRuffSettingsModal } from './LazyRuffSettingsModal'; export { default as RUFF_DEFAULT_SETTINGS } from './RuffDefaultSettings'; diff --git a/packages/console/src/notebook/Editor.tsx b/packages/console/src/notebook/Editor.tsx index f3762ccbc4..0d62b6f398 100644 --- a/packages/console/src/notebook/Editor.tsx +++ b/packages/console/src/notebook/Editor.tsx @@ -3,16 +3,16 @@ */ import React, { Component, type ReactElement } from 'react'; import classNames from 'classnames'; -import * as monaco from 'monaco-editor'; +import type * as Monaco from 'monaco-editor'; import { assertNotNull } from '@deephaven/utils'; import MonacoUtils from '../monaco/MonacoUtils'; import './Editor.scss'; export interface EditorProps { className: string; - onEditorInitialized: (editor: monaco.editor.IStandaloneCodeEditor) => void; - onEditorWillDestroy: (editor: monaco.editor.IStandaloneCodeEditor) => void; - settings: monaco.editor.IStandaloneEditorConstructionOptions; + onEditorInitialized: (editor: Monaco.editor.IStandaloneCodeEditor) => void; + onEditorWillDestroy: (editor: Monaco.editor.IStandaloneCodeEditor) => void; + settings: Monaco.editor.IStandaloneEditorConstructionOptions; } class Editor extends Component> { @@ -46,10 +46,11 @@ class Editor extends Component> { container: HTMLDivElement | null; - editor?: monaco.editor.IStandaloneCodeEditor; + editor?: Monaco.editor.IStandaloneCodeEditor; - setLanguage(language: string): void { + async setLanguage(language: string): Promise { if (this.editor) { + const monaco = await MonacoUtils.lazyMonaco(); const model = this.editor.getModel(); assertNotNull(model); monaco.editor.setModelLanguage(model, language); @@ -74,7 +75,7 @@ class Editor extends Component> { this.editor?.layout(); } - initEditor(): void { + async initEditor(): Promise { const { onEditorInitialized } = this.props; let { settings } = this.props; settings = { @@ -96,6 +97,7 @@ class Editor extends Component> { }; assertNotNull(this.container); + const monaco = await MonacoUtils.lazyMonaco(); this.editor = monaco.editor.create(this.container, settings); this.editor.addAction({ @@ -117,7 +119,7 @@ class Editor extends Component> { this.editor.layout(); monaco.languages.registerLinkProvider('plaintext', { - provideLinks: MonacoUtils.provideLinks, + provideLinks: await MonacoUtils.createProvideLinks(), }); onEditorInitialized(this.editor); diff --git a/packages/console/src/notebook/ScriptEditor.tsx b/packages/console/src/notebook/ScriptEditor.tsx index 7d245c1881..f7cb1ad600 100644 --- a/packages/console/src/notebook/ScriptEditor.tsx +++ b/packages/console/src/notebook/ScriptEditor.tsx @@ -66,40 +66,7 @@ class ScriptEditor extends Component { } componentDidUpdate(prevProps: ScriptEditorProps): void { - const { sessionLanguage, settings } = this.props; - - const language = settings?.language; - - const languageChanged = language !== prevProps.settings?.language; - if (languageChanged) { - log.debug('Set language', language); - this.setLanguage(language); - } - - const sessionDisconnected = - sessionLanguage == null && prevProps.sessionLanguage != null; - const languageMatch = language === sessionLanguage; - const prevLanguageMatch = - prevProps.settings?.language === prevProps.sessionLanguage; - if ( - sessionDisconnected || - (sessionLanguage !== undefined && prevLanguageMatch && !languageMatch) - ) { - // Session disconnected or language changed from matching the session language to non-matching - log.debug('De-init completion'); - this.deInitCodeCompletion(); - } - - const sessionConnected = - sessionLanguage != null && prevProps.sessionLanguage == null; - if ( - (sessionConnected && languageMatch) || - (sessionLanguage !== undefined && !prevLanguageMatch && languageMatch) - ) { - // Session connected with a matching language or notebook language changed to matching - log.debug('Init completion'); - this.initCodeCompletion(); - } + this.handleEditorUpdated(prevProps); } componentWillUnmount(): void { @@ -141,7 +108,9 @@ class ScriptEditor extends Component { return ScriptEditorUtils.outdentCode(model.getValueInRange(wholeLineRange)); } - handleEditorInitialized(innerEditor: editor.IStandaloneCodeEditor): void { + async handleEditorInitialized( + innerEditor: editor.IStandaloneCodeEditor + ): Promise { const { focusOnMount, onChange, @@ -156,12 +125,12 @@ class ScriptEditor extends Component { this.editor = innerEditor; this.setState({ model: this.editor.getModel() }); - MonacoUtils.setEOL(innerEditor); + await MonacoUtils.setEOL(innerEditor); MonacoUtils.registerPasteHandler(innerEditor); // Always initialize context actions when the editor is created to ensure that unwanted default // OS shortcuts are overridden by custom shortcuts. - this.initContextActions(); + await this.initContextActions(); if (session != null && settings && sessionLanguage === settings.language) { this.initCodeCompletion(); @@ -175,6 +144,43 @@ class ScriptEditor extends Component { onEditorInitialized(this.editor); } + async handleEditorUpdated(prevProps: ScriptEditorProps): Promise { + const { sessionLanguage, settings } = this.props; + + const language = settings?.language; + + const languageChanged = language !== prevProps.settings?.language; + if (languageChanged) { + log.debug('Set language', language); + await this.setLanguage(language); + } + + const sessionDisconnected = + sessionLanguage == null && prevProps.sessionLanguage != null; + const languageMatch = language === sessionLanguage; + const prevLanguageMatch = + prevProps.settings?.language === prevProps.sessionLanguage; + if ( + sessionDisconnected || + (sessionLanguage !== undefined && prevLanguageMatch && !languageMatch) + ) { + // Session disconnected or language changed from matching the session language to non-matching + log.debug('De-init completion'); + this.deInitCodeCompletion(); + } + + const sessionConnected = + sessionLanguage != null && prevProps.sessionLanguage == null; + if ( + (sessionConnected && languageMatch) || + (sessionLanguage !== undefined && !prevLanguageMatch && languageMatch) + ) { + // Session connected with a matching language or notebook language changed to matching + log.debug('Init completion'); + this.initCodeCompletion(); + } + } + handleEditorWillDestroy(innerEditor: editor.IStandaloneCodeEditor): void { log.debug('handleEditorWillDestroy'); const { onEditorWillDestroy } = this.props; @@ -225,7 +231,7 @@ class ScriptEditor extends Component { onRunCommand(command); } - initContextActions(): void { + async initContextActions(): Promise { if (this.contextActionCleanups.length > 0) { log.error('Context actions already initialized.'); return; @@ -241,7 +247,9 @@ class ScriptEditor extends Component { id: 'run-code', label: 'Run', keybindings: [ - MonacoUtils.getMonacoKeyCodeFromShortcut(SHORTCUTS.NOTEBOOK.RUN), + await MonacoUtils.getMonacoKeyCodeFromShortcut( + SHORTCUTS.NOTEBOOK.RUN + ), ], contextMenuGroupId: 'navigation', contextMenuOrder: 1.5, @@ -257,7 +265,7 @@ class ScriptEditor extends Component { id: 'run-selected-code', label: 'Run Selected', keybindings: [ - MonacoUtils.getMonacoKeyCodeFromShortcut( + await MonacoUtils.getMonacoKeyCodeFromShortcut( SHORTCUTS.NOTEBOOK.RUN_SELECTED ), ], @@ -350,9 +358,9 @@ class ScriptEditor extends Component { } } - setLanguage(language?: string): void { + async setLanguage(language?: string): Promise { if (this.editorComponent.current && language !== undefined) { - this.editorComponent.current.setLanguage(language); + await this.editorComponent.current.setLanguage(language); } } diff --git a/packages/embed-widget/package.json b/packages/embed-widget/package.json index f9dd1054a9..aab9a01d6c 100644 --- a/packages/embed-widget/package.json +++ b/packages/embed-widget/package.json @@ -45,7 +45,8 @@ "@deephaven/eslint-config": "file:../eslint-config", "@deephaven/mocks": "file:../mocks", "@deephaven/prettier-config": "file:../prettier-config", - "@deephaven/stylelint-config": "file:../stylelint-config" + "@deephaven/stylelint-config": "file:../stylelint-config", + "rollup-plugin-visualizer": "^6.0.3" }, "publishConfig": { "access": "public" diff --git a/packages/embed-widget/vite.config.ts b/packages/embed-widget/vite.config.ts index 16c8c2ed84..02ff9b4db3 100644 --- a/packages/embed-widget/vite.config.ts +++ b/packages/embed-widget/vite.config.ts @@ -1,5 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies */ import { defineConfig, loadEnv } from 'vite'; +import { visualizer } from 'rollup-plugin-visualizer'; import react from '@vitejs/plugin-react-swc'; import path from 'path'; @@ -85,6 +86,23 @@ export default defineConfig(({ mode }) => { outDir: env.VITE_BUILD_PATH, emptyOutDir: true, sourcemap: true, + // modulePreload: { + // resolveDependencies: ( + // filename: string, + // deps: string[], + // context: { + // hostId: string; + // hostType: 'html' | 'js'; + // } + // ) => { + // return []; + // // eslint-disable-next-line no-param-reassign + // deps = deps.filter(dep => !dep.includes('monaco-')); + // console.log('[TESTING]', filename, deps, context); + // return deps; + // }, + // }, + modulePreload: false, rollupOptions: { output: { manualChunks: id => { @@ -99,10 +117,22 @@ export default defineConfig(({ mode }) => { return 'helpers'; } + // if (id.includes('packages')) { + // if (id.includes('packages/console/dist')) { + // return 'deephaven-console'; + // } + // } + if (id.includes('node_modules')) { + if (id.includes('monaco-editor')) { + return 'monaco'; + } if (id.includes('plotly.js')) { return 'plotly'; } + if (id.includes('mathjax')) { + return 'mathjax'; + } return 'vendor'; } }, @@ -117,6 +147,63 @@ export default defineConfig(({ mode }) => { }, }, }, - plugins: [react()], + plugins: [ + react(), + { + /** + * Plugin to log monaco imports. Notes: + * 1. All imports show in `chunk.imports` array (even lazy ones) + * 2. Lazy imports show in `chunk.dynamicImports` array + * + * Goal would be for Monaco to always be lazy imported at least for the + * main chunk. As-is, it shows up a number of times only in the + * `chunk.imports`. Haven't been able to figure out why. + */ + name: 'log-chunk-deps', + generateBundle(options, bundle) { + // eslint-disable-next-line no-restricted-syntax + for (const [fileName, chunk] of Object.entries(bundle)) { + const isMonaco = i => i.includes('monaco-'); + + if ( + chunk.type === 'chunk' && + (chunk.imports.some(isMonaco) || + chunk.dynamicImports.some(isMonaco)) + ) { + console.log( + `Chunk: ${fileName}${chunk.isEntry ? ', isEntry' : ''}` + ); + console.log( + JSON.stringify( + { + imports: chunk.imports, + dynamicImports: chunk.dynamicImports, + }, + null, + 2 + ) + ); + } + } + }, + }, + // Different visualizations of the bundle + ( + [ + // 'flamegraph', + // 'list', + // 'network', + // 'raw-data', + // 'sunburst', + 'treemap', + ] as const + ).map(template => + visualizer({ + open: true, + filename: `stats-${template}.html`, + template, + }) + ), + ], }; }); diff --git a/packages/iris-grid/src/sidebar/InputEditor.tsx b/packages/iris-grid/src/sidebar/InputEditor.tsx index ae10feba05..824e978308 100644 --- a/packages/iris-grid/src/sidebar/InputEditor.tsx +++ b/packages/iris-grid/src/sidebar/InputEditor.tsx @@ -1,14 +1,15 @@ import React, { Component, type ReactElement } from 'react'; -import * as monaco from 'monaco-editor'; +import type * as Monaco from 'monaco-editor'; import classNames from 'classnames'; import './InputEditor.scss'; +import { MonacoUtils } from '@deephaven/console'; interface InputEditorProps { className?: string; placeholder?: string; value: string; onContentChanged: (value?: string) => void; - editorSettings: Partial; + editorSettings: Partial; editorIndex: number; onTab: (editorIndex: number, shiftKey: boolean) => void; invalid: boolean; @@ -58,9 +59,9 @@ export class InputEditor extends Component { editorContainer: HTMLDivElement | null; - editor?: monaco.editor.IStandaloneCodeEditor; + editor?: Monaco.editor.IStandaloneCodeEditor; - initEditor(): void { + async initEditor(): Promise { const { value, editorSettings } = this.props; const inputEditorSettings = { copyWithSyntaxHighlighting: 'false', @@ -90,10 +91,11 @@ export class InputEditor extends Component { automaticLayout: true, autoClosingBrackets: 'beforeWhitespace', ...editorSettings, - } as monaco.editor.IStandaloneEditorConstructionOptions; + } as Monaco.editor.IStandaloneEditorConstructionOptions; if (!this.editorContainer) { throw new Error('editorContainer is null'); } + const monaco = await MonacoUtils.lazyMonaco(); this.editor = monaco.editor.create( this.editorContainer, inputEditorSettings @@ -146,7 +148,7 @@ export class InputEditor extends Component { this.editor?.focus(); } - handleKeyDown(event: monaco.IKeyboardEvent): void { + handleKeyDown(event: Monaco.IKeyboardEvent): void { const { onTab, editorIndex } = this.props; if (event.code === 'Tab') { event.stopPropagation(); From c91aeb3c268d4f34b2834e2de3ec267a627bd93a Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Thu, 14 Aug 2025 15:54:14 -0500 Subject: [PATCH 2/2] Comments (DH-20141) --- packages/embed-widget/vite.config.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/embed-widget/vite.config.ts b/packages/embed-widget/vite.config.ts index 02ff9b4db3..64aedffd13 100644 --- a/packages/embed-widget/vite.config.ts +++ b/packages/embed-widget/vite.config.ts @@ -102,6 +102,11 @@ export default defineConfig(({ mode }) => { // return deps; // }, // }, + // TODO: This "should" disable module preload. It seems to work for + // monaco.js but not the .css. There seems to still be something eagerly + // loading it, but haven't been able to track it down yet. We probably + // don't actually want to disable this. In theory it will be fixed if we + // figure out what is importing it eagerly. modulePreload: false, rollupOptions: { output: { @@ -117,12 +122,6 @@ export default defineConfig(({ mode }) => { return 'helpers'; } - // if (id.includes('packages')) { - // if (id.includes('packages/console/dist')) { - // return 'deephaven-console'; - // } - // } - if (id.includes('node_modules')) { if (id.includes('monaco-editor')) { return 'monaco';